Statistics
| Branch: | Tag: | Revision:

root / lib / bdev.py @ 23e3c9b7

History | View | Annotate | Download (90.5 kB)

1
#
2
#
3

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

    
34
from ganeti import utils
35
from ganeti import errors
36
from ganeti import constants
37
from ganeti import objects
38
from ganeti import compat
39
from ganeti import netutils
40
from ganeti import pathutils
41

    
42

    
43
# Size of reads in _CanReadDevice
44
_DEVICE_READ_SIZE = 128 * 1024
45

    
46

    
47
def _IgnoreError(fn, *args, **kwargs):
48
  """Executes the given function, ignoring BlockDeviceErrors.
49

50
  This is used in order to simplify the execution of cleanup or
51
  rollback functions.
52

53
  @rtype: boolean
54
  @return: True when fn didn't raise an exception, False otherwise
55

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

    
64

    
65
def _ThrowError(msg, *args):
66
  """Log an error to the node daemon and the raise an exception.
67

68
  @type msg: string
69
  @param msg: the text of the exception
70
  @raise errors.BlockDeviceError
71

72
  """
73
  if args:
74
    msg = msg % args
75
  logging.error(msg)
76
  raise errors.BlockDeviceError(msg)
77

    
78

    
79
def _CheckResult(result):
80
  """Throws an error if the given result is a failed one.
81

82
  @param result: result from RunCmd
83

84
  """
85
  if result.failed:
86
    _ThrowError("Command: %s error: %s - %s", result.cmd, result.fail_reason,
87
                result.output)
88

    
89

    
90
def _CanReadDevice(path):
91
  """Check if we can read from the given device.
92

93
  This tries to read the first 128k of the device.
94

95
  """
96
  try:
97
    utils.ReadFile(path, size=_DEVICE_READ_SIZE)
98
    return True
99
  except EnvironmentError:
100
    logging.warning("Can't read from device %s", path, exc_info=True)
101
    return False
102

    
103

    
104
def _GetForbiddenFileStoragePaths():
105
  """Builds a list of path prefixes which shouldn't be used for file storage.
106

107
  @rtype: frozenset
108

109
  """
110
  paths = set([
111
    "/boot",
112
    "/dev",
113
    "/etc",
114
    "/home",
115
    "/proc",
116
    "/root",
117
    "/sys",
118
    ])
119

    
120
  for prefix in ["", "/usr", "/usr/local"]:
121
    paths.update(map(lambda s: "%s/%s" % (prefix, s),
122
                     ["bin", "lib", "lib32", "lib64", "sbin"]))
123

    
124
  return frozenset(map(os.path.normpath, paths))
125

    
126

    
127
def _ComputeWrongFileStoragePaths(paths,
128
                                  _forbidden=_GetForbiddenFileStoragePaths()):
129
  """Cross-checks a list of paths for prefixes considered bad.
130

131
  Some paths, e.g. "/bin", should not be used for file storage.
132

133
  @type paths: list
134
  @param paths: List of paths to be checked
135
  @rtype: list
136
  @return: Sorted list of paths for which the user should be warned
137

138
  """
139
  def _Check(path):
140
    return (not os.path.isabs(path) or
141
            path in _forbidden or
142
            filter(lambda p: utils.IsBelowDir(p, path), _forbidden))
143

    
144
  return utils.NiceSort(filter(_Check, map(os.path.normpath, paths)))
145

    
146

    
147
def ComputeWrongFileStoragePaths(_filename=pathutils.FILE_STORAGE_PATHS_FILE):
148
  """Returns a list of file storage paths whose prefix is considered bad.
149

150
  See L{_ComputeWrongFileStoragePaths}.
151

152
  """
153
  return _ComputeWrongFileStoragePaths(_LoadAllowedFileStoragePaths(_filename))
154

    
155

    
156
def _CheckFileStoragePath(path, allowed):
157
  """Checks if a path is in a list of allowed paths for file storage.
158

159
  @type path: string
160
  @param path: Path to check
161
  @type allowed: list
162
  @param allowed: List of allowed paths
163
  @raise errors.FileStoragePathError: If the path is not allowed
164

165
  """
166
  if not os.path.isabs(path):
167
    raise errors.FileStoragePathError("File storage path must be absolute,"
168
                                      " got '%s'" % path)
169

    
170
  for i in allowed:
171
    if not os.path.isabs(i):
172
      logging.info("Ignoring relative path '%s' for file storage", i)
173
      continue
174

    
175
    if utils.IsBelowDir(i, path):
176
      break
177
  else:
178
    raise errors.FileStoragePathError("Path '%s' is not acceptable for file"
179
                                      " storage" % path)
180

    
181

    
182
def _LoadAllowedFileStoragePaths(filename):
183
  """Loads file containing allowed file storage paths.
184

185
  @rtype: list
186
  @return: List of allowed paths (can be an empty list)
187

188
  """
189
  try:
190
    contents = utils.ReadFile(filename)
191
  except EnvironmentError:
192
    return []
193
  else:
194
    return utils.FilterEmptyLinesAndComments(contents)
195

    
196

    
197
def CheckFileStoragePath(path, _filename=pathutils.FILE_STORAGE_PATHS_FILE):
198
  """Checks if a path is allowed for file storage.
199

200
  @type path: string
201
  @param path: Path to check
202
  @raise errors.FileStoragePathError: If the path is not allowed
203

204
  """
205
  allowed = _LoadAllowedFileStoragePaths(_filename)
206

    
207
  if _ComputeWrongFileStoragePaths([path]):
208
    raise errors.FileStoragePathError("Path '%s' uses a forbidden prefix" %
209
                                      path)
210

    
211
  _CheckFileStoragePath(path, allowed)
212

    
213

    
214
class BlockDev(object):
215
  """Block device abstract class.
216

217
  A block device can be in the following states:
218
    - not existing on the system, and by `Create()` it goes into:
219
    - existing but not setup/not active, and by `Assemble()` goes into:
220
    - active read-write and by `Open()` it goes into
221
    - online (=used, or ready for use)
222

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

229
  The many different states of the device are due to the fact that we
230
  need to cover many device types:
231
    - logical volumes are created, lvchange -a y $lv, and used
232
    - drbd devices are attached to a local disk/remote peer and made primary
233

234
  A block device is identified by three items:
235
    - the /dev path of the device (dynamic)
236
    - a unique ID of the device (static)
237
    - it's major/minor pair (dynamic)
238

239
  Not all devices implement both the first two as distinct items. LVM
240
  logical volumes have their unique ID (the pair volume group, logical
241
  volume name) in a 1-to-1 relation to the dev path. For DRBD devices,
242
  the /dev path is again dynamic and the unique id is the pair (host1,
243
  dev1), (host2, dev2).
244

245
  You can get to a device in two ways:
246
    - creating the (real) device, which returns you
247
      an attached instance (lvcreate)
248
    - attaching of a python instance to an existing (real) device
249

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

256
  """
257
  def __init__(self, unique_id, children, size, params):
258
    self._children = children
259
    self.dev_path = None
260
    self.unique_id = unique_id
261
    self.major = None
262
    self.minor = None
263
    self.attached = False
264
    self.size = size
265
    self.params = params
266

    
267
  def Assemble(self):
268
    """Assemble the device from its components.
269

270
    Implementations of this method by child classes must ensure that:
271
      - after the device has been assembled, it knows its major/minor
272
        numbers; this allows other devices (usually parents) to probe
273
        correctly for their children
274
      - calling this method on an existing, in-use device is safe
275
      - if the device is already configured (and in an OK state),
276
        this method is idempotent
277

278
    """
279
    pass
280

    
281
  def Attach(self):
282
    """Find a device which matches our config and attach to it.
283

284
    """
285
    raise NotImplementedError
286

    
287
  def Close(self):
288
    """Notifies that the device will no longer be used for I/O.
289

290
    """
291
    raise NotImplementedError
292

    
293
  @classmethod
294
  def Create(cls, unique_id, children, size, params):
295
    """Create the device.
296

297
    If the device cannot be created, it will return None
298
    instead. Error messages go to the logging system.
299

300
    Note that for some devices, the unique_id is used, and for other,
301
    the children. The idea is that these two, taken together, are
302
    enough for both creation and assembly (later).
303

304
    """
305
    raise NotImplementedError
306

    
307
  def Remove(self):
308
    """Remove this device.
309

310
    This makes sense only for some of the device types: LV and file
311
    storage. Also note that if the device can't attach, the removal
312
    can't be completed.
313

314
    """
315
    raise NotImplementedError
316

    
317
  def Rename(self, new_id):
318
    """Rename this device.
319

320
    This may or may not make sense for a given device type.
321

322
    """
323
    raise NotImplementedError
324

    
325
  def Open(self, force=False):
326
    """Make the device ready for use.
327

328
    This makes the device ready for I/O. For now, just the DRBD
329
    devices need this.
330

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

334
    """
335
    raise NotImplementedError
336

    
337
  def Shutdown(self):
338
    """Shut down the device, freeing its children.
339

340
    This undoes the `Assemble()` work, except for the child
341
    assembling; as such, the children on the device are still
342
    assembled after this call.
343

344
    """
345
    raise NotImplementedError
346

    
347
  def SetSyncParams(self, params):
348
    """Adjust the synchronization parameters of the mirror.
349

350
    In case this is not a mirroring device, this is no-op.
351

352
    @param params: dictionary of LD level disk parameters related to the
353
    synchronization.
354
    @rtype: list
355
    @return: a list of error messages, emitted both by the current node and by
356
    children. An empty list means no errors.
357

358
    """
359
    result = []
360
    if self._children:
361
      for child in self._children:
362
        result.extend(child.SetSyncParams(params))
363
    return result
364

    
365
  def PauseResumeSync(self, pause):
366
    """Pause/Resume the sync of the mirror.
367

368
    In case this is not a mirroring device, this is no-op.
369

370
    @param pause: Whether to pause or resume
371

372
    """
373
    result = True
374
    if self._children:
375
      for child in self._children:
376
        result = result and child.PauseResumeSync(pause)
377
    return result
378

    
379
  def GetSyncStatus(self):
380
    """Returns the sync status of the device.
381

382
    If this device is a mirroring device, this function returns the
383
    status of the mirror.
384

385
    If sync_percent is None, it means the device is not syncing.
386

387
    If estimated_time is None, it means we can't estimate
388
    the time needed, otherwise it's the time left in seconds.
389

390
    If is_degraded is True, it means the device is missing
391
    redundancy. This is usually a sign that something went wrong in
392
    the device setup, if sync_percent is None.
393

394
    The ldisk parameter represents the degradation of the local
395
    data. This is only valid for some devices, the rest will always
396
    return False (not degraded).
397

398
    @rtype: objects.BlockDevStatus
399

400
    """
401
    return objects.BlockDevStatus(dev_path=self.dev_path,
402
                                  major=self.major,
403
                                  minor=self.minor,
404
                                  sync_percent=None,
405
                                  estimated_time=None,
406
                                  is_degraded=False,
407
                                  ldisk_status=constants.LDS_OKAY)
408

    
409
  def CombinedSyncStatus(self):
410
    """Calculate the mirror status recursively for our children.
411

412
    The return value is the same as for `GetSyncStatus()` except the
413
    minimum percent and maximum time are calculated across our
414
    children.
415

416
    @rtype: objects.BlockDevStatus
417

418
    """
419
    status = self.GetSyncStatus()
420

    
421
    min_percent = status.sync_percent
422
    max_time = status.estimated_time
423
    is_degraded = status.is_degraded
424
    ldisk_status = status.ldisk_status
425

    
426
    if self._children:
427
      for child in self._children:
428
        child_status = child.GetSyncStatus()
429

    
430
        if min_percent is None:
431
          min_percent = child_status.sync_percent
432
        elif child_status.sync_percent is not None:
433
          min_percent = min(min_percent, child_status.sync_percent)
434

    
435
        if max_time is None:
436
          max_time = child_status.estimated_time
437
        elif child_status.estimated_time is not None:
438
          max_time = max(max_time, child_status.estimated_time)
439

    
440
        is_degraded = is_degraded or child_status.is_degraded
441

    
442
        if ldisk_status is None:
443
          ldisk_status = child_status.ldisk_status
444
        elif child_status.ldisk_status is not None:
445
          ldisk_status = max(ldisk_status, child_status.ldisk_status)
446

    
447
    return objects.BlockDevStatus(dev_path=self.dev_path,
448
                                  major=self.major,
449
                                  minor=self.minor,
450
                                  sync_percent=min_percent,
451
                                  estimated_time=max_time,
452
                                  is_degraded=is_degraded,
453
                                  ldisk_status=ldisk_status)
454

    
455
  def SetInfo(self, text):
456
    """Update metadata with info text.
457

458
    Only supported for some device types.
459

460
    """
461
    for child in self._children:
462
      child.SetInfo(text)
463

    
464
  def Grow(self, amount, dryrun, backingstore):
465
    """Grow the block device.
466

467
    @type amount: integer
468
    @param amount: the amount (in mebibytes) to grow with
469
    @type dryrun: boolean
470
    @param dryrun: whether to execute the operation in simulation mode
471
        only, without actually increasing the size
472
    @param backingstore: whether to execute the operation on backing storage
473
        only, or on "logical" storage only; e.g. DRBD is logical storage,
474
        whereas LVM, file, RBD are backing storage
475

476
    """
477
    raise NotImplementedError
478

    
479
  def GetActualSize(self):
480
    """Return the actual disk size.
481

482
    @note: the device needs to be active when this is called
483

484
    """
485
    assert self.attached, "BlockDevice not attached in GetActualSize()"
486
    result = utils.RunCmd(["blockdev", "--getsize64", self.dev_path])
487
    if result.failed:
488
      _ThrowError("blockdev failed (%s): %s",
489
                  result.fail_reason, result.output)
490
    try:
491
      sz = int(result.output.strip())
492
    except (ValueError, TypeError), err:
493
      _ThrowError("Failed to parse blockdev output: %s", str(err))
494
    return sz
495

    
496
  def __repr__(self):
497
    return ("<%s: unique_id: %s, children: %s, %s:%s, %s>" %
498
            (self.__class__, self.unique_id, self._children,
499
             self.major, self.minor, self.dev_path))
500

    
501

    
502
class LogicalVolume(BlockDev):
503
  """Logical Volume block device.
504

505
  """
506
  _VALID_NAME_RE = re.compile("^[a-zA-Z0-9+_.-]*$")
507
  _INVALID_NAMES = frozenset([".", "..", "snapshot", "pvmove"])
508
  _INVALID_SUBSTRINGS = frozenset(["_mlog", "_mimage"])
509

    
510
  def __init__(self, unique_id, children, size, params):
511
    """Attaches to a LV device.
512

513
    The unique_id is a tuple (vg_name, lv_name)
514

515
    """
516
    super(LogicalVolume, self).__init__(unique_id, children, size, params)
517
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
518
      raise ValueError("Invalid configuration data %s" % str(unique_id))
519
    self._vg_name, self._lv_name = unique_id
520
    self._ValidateName(self._vg_name)
521
    self._ValidateName(self._lv_name)
522
    self.dev_path = utils.PathJoin("/dev", self._vg_name, self._lv_name)
523
    self._degraded = True
524
    self.major = self.minor = self.pe_size = self.stripe_count = None
525
    self.Attach()
526

    
527
  @classmethod
528
  def Create(cls, unique_id, children, size, params):
529
    """Create a new logical volume.
530

531
    """
532
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
533
      raise errors.ProgrammerError("Invalid configuration data %s" %
534
                                   str(unique_id))
535
    vg_name, lv_name = unique_id
536
    cls._ValidateName(vg_name)
537
    cls._ValidateName(lv_name)
538
    pvs_info = cls.GetPVInfo([vg_name])
539
    if not pvs_info:
540
      _ThrowError("Can't compute PV info for vg %s", vg_name)
541
    pvs_info.sort()
542
    pvs_info.reverse()
543

    
544
    pvlist = [pv[1] for pv in pvs_info]
545
    if compat.any(":" in v for v in pvlist):
546
      _ThrowError("Some of your PVs have the invalid character ':' in their"
547
                  " name, this is not supported - please filter them out"
548
                  " in lvm.conf using either 'filter' or 'preferred_names'")
549
    free_size = sum([pv[0] for pv in pvs_info])
550
    current_pvs = len(pvlist)
551
    desired_stripes = params[constants.LDP_STRIPES]
552
    stripes = min(current_pvs, desired_stripes)
553
    if stripes < desired_stripes:
554
      logging.warning("Could not use %d stripes for VG %s, as only %d PVs are"
555
                      " available.", desired_stripes, vg_name, current_pvs)
556

    
557
    # The size constraint should have been checked from the master before
558
    # calling the create function.
559
    if free_size < size:
560
      _ThrowError("Not enough free space: required %s,"
561
                  " available %s", size, free_size)
562
    cmd = ["lvcreate", "-L%dm" % size, "-n%s" % lv_name]
563
    # If the free space is not well distributed, we won't be able to
564
    # create an optimally-striped volume; in that case, we want to try
565
    # with N, N-1, ..., 2, and finally 1 (non-stripped) number of
566
    # stripes
567
    for stripes_arg in range(stripes, 0, -1):
568
      result = utils.RunCmd(cmd + ["-i%d" % stripes_arg] + [vg_name] + pvlist)
569
      if not result.failed:
570
        break
571
    if result.failed:
572
      _ThrowError("LV create failed (%s): %s",
573
                  result.fail_reason, result.output)
574
    return LogicalVolume(unique_id, children, size, params)
575

    
576
  @staticmethod
577
  def _GetVolumeInfo(lvm_cmd, fields):
578
    """Returns LVM Volumen infos using lvm_cmd
579

580
    @param lvm_cmd: Should be one of "pvs", "vgs" or "lvs"
581
    @param fields: Fields to return
582
    @return: A list of dicts each with the parsed fields
583

584
    """
585
    if not fields:
586
      raise errors.ProgrammerError("No fields specified")
587

    
588
    sep = "|"
589
    cmd = [lvm_cmd, "--noheadings", "--nosuffix", "--units=m", "--unbuffered",
590
           "--separator=%s" % sep, "-o%s" % ",".join(fields)]
591

    
592
    result = utils.RunCmd(cmd)
593
    if result.failed:
594
      raise errors.CommandError("Can't get the volume information: %s - %s" %
595
                                (result.fail_reason, result.output))
596

    
597
    data = []
598
    for line in result.stdout.splitlines():
599
      splitted_fields = line.strip().split(sep)
600

    
601
      if len(fields) != len(splitted_fields):
602
        raise errors.CommandError("Can't parse %s output: line '%s'" %
603
                                  (lvm_cmd, line))
604

    
605
      data.append(splitted_fields)
606

    
607
    return data
608

    
609
  @classmethod
610
  def GetPVInfo(cls, vg_names, filter_allocatable=True):
611
    """Get the free space info for PVs in a volume group.
612

613
    @param vg_names: list of volume group names, if empty all will be returned
614
    @param filter_allocatable: whether to skip over unallocatable PVs
615

616
    @rtype: list
617
    @return: list of tuples (free_space, name) with free_space in mebibytes
618

619
    """
620
    try:
621
      info = cls._GetVolumeInfo("pvs", ["pv_name", "vg_name", "pv_free",
622
                                        "pv_attr"])
623
    except errors.GenericError, err:
624
      logging.error("Can't get PV information: %s", err)
625
      return None
626

    
627
    data = []
628
    for pv_name, vg_name, pv_free, pv_attr in info:
629
      # (possibly) skip over pvs which are not allocatable
630
      if filter_allocatable and pv_attr[0] != "a":
631
        continue
632
      # (possibly) skip over pvs which are not in the right volume group(s)
633
      if vg_names and vg_name not in vg_names:
634
        continue
635
      data.append((float(pv_free), pv_name, vg_name))
636

    
637
    return data
638

    
639
  @classmethod
640
  def GetVGInfo(cls, vg_names, filter_readonly=True):
641
    """Get the free space info for specific VGs.
642

643
    @param vg_names: list of volume group names, if empty all will be returned
644
    @param filter_readonly: whether to skip over readonly VGs
645

646
    @rtype: list
647
    @return: list of tuples (free_space, total_size, name) with free_space in
648
             MiB
649

650
    """
651
    try:
652
      info = cls._GetVolumeInfo("vgs", ["vg_name", "vg_free", "vg_attr",
653
                                        "vg_size"])
654
    except errors.GenericError, err:
655
      logging.error("Can't get VG information: %s", err)
656
      return None
657

    
658
    data = []
659
    for vg_name, vg_free, vg_attr, vg_size in info:
660
      # (possibly) skip over vgs which are not writable
661
      if filter_readonly and vg_attr[0] == "r":
662
        continue
663
      # (possibly) skip over vgs which are not in the right volume group(s)
664
      if vg_names and vg_name not in vg_names:
665
        continue
666
      data.append((float(vg_free), float(vg_size), vg_name))
667

    
668
    return data
669

    
670
  @classmethod
671
  def _ValidateName(cls, name):
672
    """Validates that a given name is valid as VG or LV name.
673

674
    The list of valid characters and restricted names is taken out of
675
    the lvm(8) manpage, with the simplification that we enforce both
676
    VG and LV restrictions on the names.
677

678
    """
679
    if (not cls._VALID_NAME_RE.match(name) or
680
        name in cls._INVALID_NAMES or
681
        compat.any(substring in name for substring in cls._INVALID_SUBSTRINGS)):
682
      _ThrowError("Invalid LVM name '%s'", name)
683

    
684
  def Remove(self):
685
    """Remove this logical volume.
686

687
    """
688
    if not self.minor and not self.Attach():
689
      # the LV does not exist
690
      return
691
    result = utils.RunCmd(["lvremove", "-f", "%s/%s" %
692
                           (self._vg_name, self._lv_name)])
693
    if result.failed:
694
      _ThrowError("Can't lvremove: %s - %s", result.fail_reason, result.output)
695

    
696
  def Rename(self, new_id):
697
    """Rename this logical volume.
698

699
    """
700
    if not isinstance(new_id, (tuple, list)) or len(new_id) != 2:
701
      raise errors.ProgrammerError("Invalid new logical id '%s'" % new_id)
702
    new_vg, new_name = new_id
703
    if new_vg != self._vg_name:
704
      raise errors.ProgrammerError("Can't move a logical volume across"
705
                                   " volume groups (from %s to to %s)" %
706
                                   (self._vg_name, new_vg))
707
    result = utils.RunCmd(["lvrename", new_vg, self._lv_name, new_name])
708
    if result.failed:
709
      _ThrowError("Failed to rename the logical volume: %s", result.output)
710
    self._lv_name = new_name
711
    self.dev_path = utils.PathJoin("/dev", self._vg_name, self._lv_name)
712

    
713
  def Attach(self):
714
    """Attach to an existing LV.
715

716
    This method will try to see if an existing and active LV exists
717
    which matches our name. If so, its major/minor will be
718
    recorded.
719

720
    """
721
    self.attached = False
722
    result = utils.RunCmd(["lvs", "--noheadings", "--separator=,",
723
                           "--units=m", "--nosuffix",
724
                           "-olv_attr,lv_kernel_major,lv_kernel_minor,"
725
                           "vg_extent_size,stripes", self.dev_path])
726
    if result.failed:
727
      logging.error("Can't find LV %s: %s, %s",
728
                    self.dev_path, result.fail_reason, result.output)
729
      return False
730
    # the output can (and will) have multiple lines for multi-segment
731
    # LVs, as the 'stripes' parameter is a segment one, so we take
732
    # only the last entry, which is the one we're interested in; note
733
    # that with LVM2 anyway the 'stripes' value must be constant
734
    # across segments, so this is a no-op actually
735
    out = result.stdout.splitlines()
736
    if not out: # totally empty result? splitlines() returns at least
737
                # one line for any non-empty string
738
      logging.error("Can't parse LVS output, no lines? Got '%s'", str(out))
739
      return False
740
    out = out[-1].strip().rstrip(",")
741
    out = out.split(",")
742
    if len(out) != 5:
743
      logging.error("Can't parse LVS output, len(%s) != 5", str(out))
744
      return False
745

    
746
    status, major, minor, pe_size, stripes = out
747
    if len(status) < 6:
748
      logging.error("lvs lv_attr is not at least 6 characters (%s)", status)
749
      return False
750

    
751
    try:
752
      major = int(major)
753
      minor = int(minor)
754
    except (TypeError, ValueError), err:
755
      logging.error("lvs major/minor cannot be parsed: %s", str(err))
756

    
757
    try:
758
      pe_size = int(float(pe_size))
759
    except (TypeError, ValueError), err:
760
      logging.error("Can't parse vg extent size: %s", err)
761
      return False
762

    
763
    try:
764
      stripes = int(stripes)
765
    except (TypeError, ValueError), err:
766
      logging.error("Can't parse the number of stripes: %s", err)
767
      return False
768

    
769
    self.major = major
770
    self.minor = minor
771
    self.pe_size = pe_size
772
    self.stripe_count = stripes
773
    self._degraded = status[0] == "v" # virtual volume, i.e. doesn't backing
774
                                      # storage
775
    self.attached = True
776
    return True
777

    
778
  def Assemble(self):
779
    """Assemble the device.
780

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

785
    """
786
    result = utils.RunCmd(["lvchange", "-ay", self.dev_path])
787
    if result.failed:
788
      _ThrowError("Can't activate lv %s: %s", self.dev_path, result.output)
789

    
790
  def Shutdown(self):
791
    """Shutdown the device.
792

793
    This is a no-op for the LV device type, as we don't deactivate the
794
    volumes on shutdown.
795

796
    """
797
    pass
798

    
799
  def GetSyncStatus(self):
800
    """Returns the sync status of the device.
801

802
    If this device is a mirroring device, this function returns the
803
    status of the mirror.
804

805
    For logical volumes, sync_percent and estimated_time are always
806
    None (no recovery in progress, as we don't handle the mirrored LV
807
    case). The is_degraded parameter is the inverse of the ldisk
808
    parameter.
809

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

816
    The status was already read in Attach, so we just return it.
817

818
    @rtype: objects.BlockDevStatus
819

820
    """
821
    if self._degraded:
822
      ldisk_status = constants.LDS_FAULTY
823
    else:
824
      ldisk_status = constants.LDS_OKAY
825

    
826
    return objects.BlockDevStatus(dev_path=self.dev_path,
827
                                  major=self.major,
828
                                  minor=self.minor,
829
                                  sync_percent=None,
830
                                  estimated_time=None,
831
                                  is_degraded=self._degraded,
832
                                  ldisk_status=ldisk_status)
833

    
834
  def Open(self, force=False):
835
    """Make the device ready for I/O.
836

837
    This is a no-op for the LV device type.
838

839
    """
840
    pass
841

    
842
  def Close(self):
843
    """Notifies that the device will no longer be used for I/O.
844

845
    This is a no-op for the LV device type.
846

847
    """
848
    pass
849

    
850
  def Snapshot(self, size):
851
    """Create a snapshot copy of an lvm block device.
852

853
    @returns: tuple (vg, lv)
854

855
    """
856
    snap_name = self._lv_name + ".snap"
857

    
858
    # remove existing snapshot if found
859
    snap = LogicalVolume((self._vg_name, snap_name), None, size, self.params)
860
    _IgnoreError(snap.Remove)
861

    
862
    vg_info = self.GetVGInfo([self._vg_name])
863
    if not vg_info:
864
      _ThrowError("Can't compute VG info for vg %s", self._vg_name)
865
    free_size, _, _ = vg_info[0]
866
    if free_size < size:
867
      _ThrowError("Not enough free space: required %s,"
868
                  " available %s", size, free_size)
869

    
870
    _CheckResult(utils.RunCmd(["lvcreate", "-L%dm" % size, "-s",
871
                               "-n%s" % snap_name, self.dev_path]))
872

    
873
    return (self._vg_name, snap_name)
874

    
875
  def _RemoveOldInfo(self):
876
    """Try to remove old tags from the lv.
877

878
    """
879
    result = utils.RunCmd(["lvs", "-o", "tags", "--noheadings", "--nosuffix",
880
                           self.dev_path])
881
    _CheckResult(result)
882

    
883
    raw_tags = result.stdout.strip()
884
    if raw_tags:
885
      for tag in raw_tags.split(","):
886
        _CheckResult(utils.RunCmd(["lvchange", "--deltag",
887
                                   tag.strip(), self.dev_path]))
888

    
889
  def SetInfo(self, text):
890
    """Update metadata with info text.
891

892
    """
893
    BlockDev.SetInfo(self, text)
894

    
895
    self._RemoveOldInfo()
896

    
897
    # Replace invalid characters
898
    text = re.sub("^[^A-Za-z0-9_+.]", "_", text)
899
    text = re.sub("[^-A-Za-z0-9_+.]", "_", text)
900

    
901
    # Only up to 128 characters are allowed
902
    text = text[:128]
903

    
904
    _CheckResult(utils.RunCmd(["lvchange", "--addtag", text, self.dev_path]))
905

    
906
  def Grow(self, amount, dryrun, backingstore):
907
    """Grow the logical volume.
908

909
    """
910
    if not backingstore:
911
      return
912
    if self.pe_size is None or self.stripe_count is None:
913
      if not self.Attach():
914
        _ThrowError("Can't attach to LV during Grow()")
915
    full_stripe_size = self.pe_size * self.stripe_count
916
    rest = amount % full_stripe_size
917
    if rest != 0:
918
      amount += full_stripe_size - rest
919
    cmd = ["lvextend", "-L", "+%dm" % amount]
920
    if dryrun:
921
      cmd.append("--test")
922
    # we try multiple algorithms since the 'best' ones might not have
923
    # space available in the right place, but later ones might (since
924
    # they have less constraints); also note that only recent LVM
925
    # supports 'cling'
926
    for alloc_policy in "contiguous", "cling", "normal":
927
      result = utils.RunCmd(cmd + ["--alloc", alloc_policy, self.dev_path])
928
      if not result.failed:
929
        return
930
    _ThrowError("Can't grow LV %s: %s", self.dev_path, result.output)
931

    
932

    
933
class DRBD8Status(object):
934
  """A DRBD status representation class.
935

936
  Note that this doesn't support unconfigured devices (cs:Unconfigured).
937

938
  """
939
  UNCONF_RE = re.compile(r"\s*[0-9]+:\s*cs:Unconfigured$")
940
  LINE_RE = re.compile(r"\s*[0-9]+:\s*cs:(\S+)\s+(?:st|ro):([^/]+)/(\S+)"
941
                       "\s+ds:([^/]+)/(\S+)\s+.*$")
942
  SYNC_RE = re.compile(r"^.*\ssync'ed:\s*([0-9.]+)%.*"
943
                       # Due to a bug in drbd in the kernel, introduced in
944
                       # commit 4b0715f096 (still unfixed as of 2011-08-22)
945
                       "(?:\s|M)"
946
                       "finish: ([0-9]+):([0-9]+):([0-9]+)\s.*$")
947

    
948
  CS_UNCONFIGURED = "Unconfigured"
949
  CS_STANDALONE = "StandAlone"
950
  CS_WFCONNECTION = "WFConnection"
951
  CS_WFREPORTPARAMS = "WFReportParams"
952
  CS_CONNECTED = "Connected"
953
  CS_STARTINGSYNCS = "StartingSyncS"
954
  CS_STARTINGSYNCT = "StartingSyncT"
955
  CS_WFBITMAPS = "WFBitMapS"
956
  CS_WFBITMAPT = "WFBitMapT"
957
  CS_WFSYNCUUID = "WFSyncUUID"
958
  CS_SYNCSOURCE = "SyncSource"
959
  CS_SYNCTARGET = "SyncTarget"
960
  CS_PAUSEDSYNCS = "PausedSyncS"
961
  CS_PAUSEDSYNCT = "PausedSyncT"
962
  CSET_SYNC = frozenset([
963
    CS_WFREPORTPARAMS,
964
    CS_STARTINGSYNCS,
965
    CS_STARTINGSYNCT,
966
    CS_WFBITMAPS,
967
    CS_WFBITMAPT,
968
    CS_WFSYNCUUID,
969
    CS_SYNCSOURCE,
970
    CS_SYNCTARGET,
971
    CS_PAUSEDSYNCS,
972
    CS_PAUSEDSYNCT,
973
    ])
974

    
975
  DS_DISKLESS = "Diskless"
976
  DS_ATTACHING = "Attaching" # transient state
977
  DS_FAILED = "Failed" # transient state, next: diskless
978
  DS_NEGOTIATING = "Negotiating" # transient state
979
  DS_INCONSISTENT = "Inconsistent" # while syncing or after creation
980
  DS_OUTDATED = "Outdated"
981
  DS_DUNKNOWN = "DUnknown" # shown for peer disk when not connected
982
  DS_CONSISTENT = "Consistent"
983
  DS_UPTODATE = "UpToDate" # normal state
984

    
985
  RO_PRIMARY = "Primary"
986
  RO_SECONDARY = "Secondary"
987
  RO_UNKNOWN = "Unknown"
988

    
989
  def __init__(self, procline):
990
    u = self.UNCONF_RE.match(procline)
991
    if u:
992
      self.cstatus = self.CS_UNCONFIGURED
993
      self.lrole = self.rrole = self.ldisk = self.rdisk = None
994
    else:
995
      m = self.LINE_RE.match(procline)
996
      if not m:
997
        raise errors.BlockDeviceError("Can't parse input data '%s'" % procline)
998
      self.cstatus = m.group(1)
999
      self.lrole = m.group(2)
1000
      self.rrole = m.group(3)
1001
      self.ldisk = m.group(4)
1002
      self.rdisk = m.group(5)
1003

    
1004
    # end reading of data from the LINE_RE or UNCONF_RE
1005

    
1006
    self.is_standalone = self.cstatus == self.CS_STANDALONE
1007
    self.is_wfconn = self.cstatus == self.CS_WFCONNECTION
1008
    self.is_connected = self.cstatus == self.CS_CONNECTED
1009
    self.is_primary = self.lrole == self.RO_PRIMARY
1010
    self.is_secondary = self.lrole == self.RO_SECONDARY
1011
    self.peer_primary = self.rrole == self.RO_PRIMARY
1012
    self.peer_secondary = self.rrole == self.RO_SECONDARY
1013
    self.both_primary = self.is_primary and self.peer_primary
1014
    self.both_secondary = self.is_secondary and self.peer_secondary
1015

    
1016
    self.is_diskless = self.ldisk == self.DS_DISKLESS
1017
    self.is_disk_uptodate = self.ldisk == self.DS_UPTODATE
1018

    
1019
    self.is_in_resync = self.cstatus in self.CSET_SYNC
1020
    self.is_in_use = self.cstatus != self.CS_UNCONFIGURED
1021

    
1022
    m = self.SYNC_RE.match(procline)
1023
    if m:
1024
      self.sync_percent = float(m.group(1))
1025
      hours = int(m.group(2))
1026
      minutes = int(m.group(3))
1027
      seconds = int(m.group(4))
1028
      self.est_time = hours * 3600 + minutes * 60 + seconds
1029
    else:
1030
      # we have (in this if branch) no percent information, but if
1031
      # we're resyncing we need to 'fake' a sync percent information,
1032
      # as this is how cmdlib determines if it makes sense to wait for
1033
      # resyncing or not
1034
      if self.is_in_resync:
1035
        self.sync_percent = 0
1036
      else:
1037
        self.sync_percent = None
1038
      self.est_time = None
1039

    
1040

    
1041
class BaseDRBD(BlockDev): # pylint: disable=W0223
1042
  """Base DRBD class.
1043

1044
  This class contains a few bits of common functionality between the
1045
  0.7 and 8.x versions of DRBD.
1046

1047
  """
1048
  _VERSION_RE = re.compile(r"^version: (\d+)\.(\d+)\.(\d+)(?:\.\d+)?"
1049
                           r" \(api:(\d+)/proto:(\d+)(?:-(\d+))?\)")
1050
  _VALID_LINE_RE = re.compile("^ *([0-9]+): cs:([^ ]+).*$")
1051
  _UNUSED_LINE_RE = re.compile("^ *([0-9]+): cs:Unconfigured$")
1052

    
1053
  _DRBD_MAJOR = 147
1054
  _ST_UNCONFIGURED = "Unconfigured"
1055
  _ST_WFCONNECTION = "WFConnection"
1056
  _ST_CONNECTED = "Connected"
1057

    
1058
  _STATUS_FILE = "/proc/drbd"
1059
  _USERMODE_HELPER_FILE = "/sys/module/drbd/parameters/usermode_helper"
1060

    
1061
  @staticmethod
1062
  def _GetProcData(filename=_STATUS_FILE):
1063
    """Return data from /proc/drbd.
1064

1065
    """
1066
    try:
1067
      data = utils.ReadFile(filename).splitlines()
1068
    except EnvironmentError, err:
1069
      if err.errno == errno.ENOENT:
1070
        _ThrowError("The file %s cannot be opened, check if the module"
1071
                    " is loaded (%s)", filename, str(err))
1072
      else:
1073
        _ThrowError("Can't read the DRBD proc file %s: %s", filename, str(err))
1074
    if not data:
1075
      _ThrowError("Can't read any data from %s", filename)
1076
    return data
1077

    
1078
  @classmethod
1079
  def _MassageProcData(cls, data):
1080
    """Transform the output of _GetProdData into a nicer form.
1081

1082
    @return: a dictionary of minor: joined lines from /proc/drbd
1083
        for that minor
1084

1085
    """
1086
    results = {}
1087
    old_minor = old_line = None
1088
    for line in data:
1089
      if not line: # completely empty lines, as can be returned by drbd8.0+
1090
        continue
1091
      lresult = cls._VALID_LINE_RE.match(line)
1092
      if lresult is not None:
1093
        if old_minor is not None:
1094
          results[old_minor] = old_line
1095
        old_minor = int(lresult.group(1))
1096
        old_line = line
1097
      else:
1098
        if old_minor is not None:
1099
          old_line += " " + line.strip()
1100
    # add last line
1101
    if old_minor is not None:
1102
      results[old_minor] = old_line
1103
    return results
1104

    
1105
  @classmethod
1106
  def _GetVersion(cls, proc_data):
1107
    """Return the DRBD version.
1108

1109
    This will return a dict with keys:
1110
      - k_major
1111
      - k_minor
1112
      - k_point
1113
      - api
1114
      - proto
1115
      - proto2 (only on drbd > 8.2.X)
1116

1117
    """
1118
    first_line = proc_data[0].strip()
1119
    version = cls._VERSION_RE.match(first_line)
1120
    if not version:
1121
      raise errors.BlockDeviceError("Can't parse DRBD version from '%s'" %
1122
                                    first_line)
1123

    
1124
    values = version.groups()
1125
    retval = {
1126
      "k_major": int(values[0]),
1127
      "k_minor": int(values[1]),
1128
      "k_point": int(values[2]),
1129
      "api": int(values[3]),
1130
      "proto": int(values[4]),
1131
      }
1132
    if values[5] is not None:
1133
      retval["proto2"] = values[5]
1134

    
1135
    return retval
1136

    
1137
  @staticmethod
1138
  def GetUsermodeHelper(filename=_USERMODE_HELPER_FILE):
1139
    """Returns DRBD usermode_helper currently set.
1140

1141
    """
1142
    try:
1143
      helper = utils.ReadFile(filename).splitlines()[0]
1144
    except EnvironmentError, err:
1145
      if err.errno == errno.ENOENT:
1146
        _ThrowError("The file %s cannot be opened, check if the module"
1147
                    " is loaded (%s)", filename, str(err))
1148
      else:
1149
        _ThrowError("Can't read DRBD helper file %s: %s", filename, str(err))
1150
    if not helper:
1151
      _ThrowError("Can't read any data from %s", filename)
1152
    return helper
1153

    
1154
  @staticmethod
1155
  def _DevPath(minor):
1156
    """Return the path to a drbd device for a given minor.
1157

1158
    """
1159
    return "/dev/drbd%d" % minor
1160

    
1161
  @classmethod
1162
  def GetUsedDevs(cls):
1163
    """Compute the list of used DRBD devices.
1164

1165
    """
1166
    data = cls._GetProcData()
1167

    
1168
    used_devs = {}
1169
    for line in data:
1170
      match = cls._VALID_LINE_RE.match(line)
1171
      if not match:
1172
        continue
1173
      minor = int(match.group(1))
1174
      state = match.group(2)
1175
      if state == cls._ST_UNCONFIGURED:
1176
        continue
1177
      used_devs[minor] = state, line
1178

    
1179
    return used_devs
1180

    
1181
  def _SetFromMinor(self, minor):
1182
    """Set our parameters based on the given minor.
1183

1184
    This sets our minor variable and our dev_path.
1185

1186
    """
1187
    if minor is None:
1188
      self.minor = self.dev_path = None
1189
      self.attached = False
1190
    else:
1191
      self.minor = minor
1192
      self.dev_path = self._DevPath(minor)
1193
      self.attached = True
1194

    
1195
  @staticmethod
1196
  def _CheckMetaSize(meta_device):
1197
    """Check if the given meta device looks like a valid one.
1198

1199
    This currently only checks the size, which must be around
1200
    128MiB.
1201

1202
    """
1203
    result = utils.RunCmd(["blockdev", "--getsize", meta_device])
1204
    if result.failed:
1205
      _ThrowError("Failed to get device size: %s - %s",
1206
                  result.fail_reason, result.output)
1207
    try:
1208
      sectors = int(result.stdout)
1209
    except (TypeError, ValueError):
1210
      _ThrowError("Invalid output from blockdev: '%s'", result.stdout)
1211
    num_bytes = sectors * 512
1212
    if num_bytes < 128 * 1024 * 1024: # less than 128MiB
1213
      _ThrowError("Meta device too small (%.2fMib)", (num_bytes / 1024 / 1024))
1214
    # the maximum *valid* size of the meta device when living on top
1215
    # of LVM is hard to compute: it depends on the number of stripes
1216
    # and the PE size; e.g. a 2-stripe, 64MB PE will result in a 128MB
1217
    # (normal size), but an eight-stripe 128MB PE will result in a 1GB
1218
    # size meta device; as such, we restrict it to 1GB (a little bit
1219
    # too generous, but making assumptions about PE size is hard)
1220
    if num_bytes > 1024 * 1024 * 1024:
1221
      _ThrowError("Meta device too big (%.2fMiB)", (num_bytes / 1024 / 1024))
1222

    
1223
  def Rename(self, new_id):
1224
    """Rename a device.
1225

1226
    This is not supported for drbd devices.
1227

1228
    """
1229
    raise errors.ProgrammerError("Can't rename a drbd device")
1230

    
1231

    
1232
class DRBD8(BaseDRBD):
1233
  """DRBD v8.x block device.
1234

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

1239
  The unique_id for the drbd device is a (local_ip, local_port,
1240
  remote_ip, remote_port, local_minor, secret) tuple, and it must have
1241
  two children: the data device and the meta_device. The meta device
1242
  is checked for valid size and is zeroed on create.
1243

1244
  """
1245
  _MAX_MINORS = 255
1246
  _PARSE_SHOW = None
1247

    
1248
  # timeout constants
1249
  _NET_RECONFIG_TIMEOUT = 60
1250

    
1251
  # command line options for barriers
1252
  _DISABLE_DISK_OPTION = "--no-disk-barrier"  # -a
1253
  _DISABLE_DRAIN_OPTION = "--no-disk-drain"   # -D
1254
  _DISABLE_FLUSH_OPTION = "--no-disk-flushes" # -i
1255
  _DISABLE_META_FLUSH_OPTION = "--no-md-flushes"  # -m
1256

    
1257
  def __init__(self, unique_id, children, size, params):
1258
    if children and children.count(None) > 0:
1259
      children = []
1260
    if len(children) not in (0, 2):
1261
      raise ValueError("Invalid configuration data %s" % str(children))
1262
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 6:
1263
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1264
    (self._lhost, self._lport,
1265
     self._rhost, self._rport,
1266
     self._aminor, self._secret) = unique_id
1267
    if children:
1268
      if not _CanReadDevice(children[1].dev_path):
1269
        logging.info("drbd%s: Ignoring unreadable meta device", self._aminor)
1270
        children = []
1271
    super(DRBD8, self).__init__(unique_id, children, size, params)
1272
    self.major = self._DRBD_MAJOR
1273
    version = self._GetVersion(self._GetProcData())
1274
    if version["k_major"] != 8:
1275
      _ThrowError("Mismatch in DRBD kernel version and requested ganeti"
1276
                  " usage: kernel is %s.%s, ganeti wants 8.x",
1277
                  version["k_major"], version["k_minor"])
1278

    
1279
    if (self._lhost is not None and self._lhost == self._rhost and
1280
        self._lport == self._rport):
1281
      raise ValueError("Invalid configuration data, same local/remote %s" %
1282
                       (unique_id,))
1283
    self.Attach()
1284

    
1285
  @classmethod
1286
  def _InitMeta(cls, minor, dev_path):
1287
    """Initialize a meta device.
1288

1289
    This will not work if the given minor is in use.
1290

1291
    """
1292
    # Zero the metadata first, in order to make sure drbdmeta doesn't
1293
    # try to auto-detect existing filesystems or similar (see
1294
    # http://code.google.com/p/ganeti/issues/detail?id=182); we only
1295
    # care about the first 128MB of data in the device, even though it
1296
    # can be bigger
1297
    result = utils.RunCmd([constants.DD_CMD,
1298
                           "if=/dev/zero", "of=%s" % dev_path,
1299
                           "bs=1048576", "count=128", "oflag=direct"])
1300
    if result.failed:
1301
      _ThrowError("Can't wipe the meta device: %s", result.output)
1302

    
1303
    result = utils.RunCmd(["drbdmeta", "--force", cls._DevPath(minor),
1304
                           "v08", dev_path, "0", "create-md"])
1305
    if result.failed:
1306
      _ThrowError("Can't initialize meta device: %s", result.output)
1307

    
1308
  @classmethod
1309
  def _FindUnusedMinor(cls):
1310
    """Find an unused DRBD device.
1311

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

1315
    """
1316
    data = cls._GetProcData()
1317

    
1318
    highest = None
1319
    for line in data:
1320
      match = cls._UNUSED_LINE_RE.match(line)
1321
      if match:
1322
        return int(match.group(1))
1323
      match = cls._VALID_LINE_RE.match(line)
1324
      if match:
1325
        minor = int(match.group(1))
1326
        highest = max(highest, minor)
1327
    if highest is None: # there are no minors in use at all
1328
      return 0
1329
    if highest >= cls._MAX_MINORS:
1330
      logging.error("Error: no free drbd minors!")
1331
      raise errors.BlockDeviceError("Can't find a free DRBD minor")
1332
    return highest + 1
1333

    
1334
  @classmethod
1335
  def _GetShowParser(cls):
1336
    """Return a parser for `drbd show` output.
1337

1338
    This will either create or return an already-created parser for the
1339
    output of the command `drbd show`.
1340

1341
    """
1342
    if cls._PARSE_SHOW is not None:
1343
      return cls._PARSE_SHOW
1344

    
1345
    # pyparsing setup
1346
    lbrace = pyp.Literal("{").suppress()
1347
    rbrace = pyp.Literal("}").suppress()
1348
    lbracket = pyp.Literal("[").suppress()
1349
    rbracket = pyp.Literal("]").suppress()
1350
    semi = pyp.Literal(";").suppress()
1351
    colon = pyp.Literal(":").suppress()
1352
    # this also converts the value to an int
1353
    number = pyp.Word(pyp.nums).setParseAction(lambda s, l, t: int(t[0]))
1354

    
1355
    comment = pyp.Literal("#") + pyp.Optional(pyp.restOfLine)
1356
    defa = pyp.Literal("_is_default").suppress()
1357
    dbl_quote = pyp.Literal('"').suppress()
1358

    
1359
    keyword = pyp.Word(pyp.alphanums + "-")
1360

    
1361
    # value types
1362
    value = pyp.Word(pyp.alphanums + "_-/.:")
1363
    quoted = dbl_quote + pyp.CharsNotIn('"') + dbl_quote
1364
    ipv4_addr = (pyp.Optional(pyp.Literal("ipv4")).suppress() +
1365
                 pyp.Word(pyp.nums + ".") + colon + number)
1366
    ipv6_addr = (pyp.Optional(pyp.Literal("ipv6")).suppress() +
1367
                 pyp.Optional(lbracket) + pyp.Word(pyp.hexnums + ":") +
1368
                 pyp.Optional(rbracket) + colon + number)
1369
    # meta device, extended syntax
1370
    meta_value = ((value ^ quoted) + lbracket + number + rbracket)
1371
    # device name, extended syntax
1372
    device_value = pyp.Literal("minor").suppress() + number
1373

    
1374
    # a statement
1375
    stmt = (~rbrace + keyword + ~lbrace +
1376
            pyp.Optional(ipv4_addr ^ ipv6_addr ^ value ^ quoted ^ meta_value ^
1377
                         device_value) +
1378
            pyp.Optional(defa) + semi +
1379
            pyp.Optional(pyp.restOfLine).suppress())
1380

    
1381
    # an entire section
1382
    section_name = pyp.Word(pyp.alphas + "_")
1383
    section = section_name + lbrace + pyp.ZeroOrMore(pyp.Group(stmt)) + rbrace
1384

    
1385
    bnf = pyp.ZeroOrMore(pyp.Group(section ^ stmt))
1386
    bnf.ignore(comment)
1387

    
1388
    cls._PARSE_SHOW = bnf
1389

    
1390
    return bnf
1391

    
1392
  @classmethod
1393
  def _GetShowData(cls, minor):
1394
    """Return the `drbdsetup show` data for a minor.
1395

1396
    """
1397
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "show"])
1398
    if result.failed:
1399
      logging.error("Can't display the drbd config: %s - %s",
1400
                    result.fail_reason, result.output)
1401
      return None
1402
    return result.stdout
1403

    
1404
  @classmethod
1405
  def _GetDevInfo(cls, out):
1406
    """Parse details about a given DRBD minor.
1407

1408
    This return, if available, the local backing device (as a path)
1409
    and the local and remote (ip, port) information from a string
1410
    containing the output of the `drbdsetup show` command as returned
1411
    by _GetShowData.
1412

1413
    """
1414
    data = {}
1415
    if not out:
1416
      return data
1417

    
1418
    bnf = cls._GetShowParser()
1419
    # run pyparse
1420

    
1421
    try:
1422
      results = bnf.parseString(out)
1423
    except pyp.ParseException, err:
1424
      _ThrowError("Can't parse drbdsetup show output: %s", str(err))
1425

    
1426
    # and massage the results into our desired format
1427
    for section in results:
1428
      sname = section[0]
1429
      if sname == "_this_host":
1430
        for lst in section[1:]:
1431
          if lst[0] == "disk":
1432
            data["local_dev"] = lst[1]
1433
          elif lst[0] == "meta-disk":
1434
            data["meta_dev"] = lst[1]
1435
            data["meta_index"] = lst[2]
1436
          elif lst[0] == "address":
1437
            data["local_addr"] = tuple(lst[1:])
1438
      elif sname == "_remote_host":
1439
        for lst in section[1:]:
1440
          if lst[0] == "address":
1441
            data["remote_addr"] = tuple(lst[1:])
1442
    return data
1443

    
1444
  def _MatchesLocal(self, info):
1445
    """Test if our local config matches with an existing device.
1446

1447
    The parameter should be as returned from `_GetDevInfo()`. This
1448
    method tests if our local backing device is the same as the one in
1449
    the info parameter, in effect testing if we look like the given
1450
    device.
1451

1452
    """
1453
    if self._children:
1454
      backend, meta = self._children
1455
    else:
1456
      backend = meta = None
1457

    
1458
    if backend is not None:
1459
      retval = ("local_dev" in info and info["local_dev"] == backend.dev_path)
1460
    else:
1461
      retval = ("local_dev" not in info)
1462

    
1463
    if meta is not None:
1464
      retval = retval and ("meta_dev" in info and
1465
                           info["meta_dev"] == meta.dev_path)
1466
      retval = retval and ("meta_index" in info and
1467
                           info["meta_index"] == 0)
1468
    else:
1469
      retval = retval and ("meta_dev" not in info and
1470
                           "meta_index" not in info)
1471
    return retval
1472

    
1473
  def _MatchesNet(self, info):
1474
    """Test if our network config matches with an existing device.
1475

1476
    The parameter should be as returned from `_GetDevInfo()`. This
1477
    method tests if our network configuration is the same as the one
1478
    in the info parameter, in effect testing if we look like the given
1479
    device.
1480

1481
    """
1482
    if (((self._lhost is None and not ("local_addr" in info)) and
1483
         (self._rhost is None and not ("remote_addr" in info)))):
1484
      return True
1485

    
1486
    if self._lhost is None:
1487
      return False
1488

    
1489
    if not ("local_addr" in info and
1490
            "remote_addr" in info):
1491
      return False
1492

    
1493
    retval = (info["local_addr"] == (self._lhost, self._lport))
1494
    retval = (retval and
1495
              info["remote_addr"] == (self._rhost, self._rport))
1496
    return retval
1497

    
1498
  def _AssembleLocal(self, minor, backend, meta, size):
1499
    """Configure the local part of a DRBD device.
1500

1501
    """
1502
    args = ["drbdsetup", self._DevPath(minor), "disk",
1503
            backend, meta, "0",
1504
            "-e", "detach",
1505
            "--create-device"]
1506
    if size:
1507
      args.extend(["-d", "%sm" % size])
1508

    
1509
    version = self._GetVersion(self._GetProcData())
1510
    vmaj = version["k_major"]
1511
    vmin = version["k_minor"]
1512
    vrel = version["k_point"]
1513

    
1514
    barrier_args = \
1515
      self._ComputeDiskBarrierArgs(vmaj, vmin, vrel,
1516
                                   self.params[constants.LDP_BARRIERS],
1517
                                   self.params[constants.LDP_NO_META_FLUSH])
1518
    args.extend(barrier_args)
1519

    
1520
    if self.params[constants.LDP_DISK_CUSTOM]:
1521
      args.extend(shlex.split(self.params[constants.LDP_DISK_CUSTOM]))
1522

    
1523
    result = utils.RunCmd(args)
1524
    if result.failed:
1525
      _ThrowError("drbd%d: can't attach local disk: %s", minor, result.output)
1526

    
1527
  @classmethod
1528
  def _ComputeDiskBarrierArgs(cls, vmaj, vmin, vrel, disabled_barriers,
1529
                              disable_meta_flush):
1530
    """Compute the DRBD command line parameters for disk barriers
1531

1532
    Returns a list of the disk barrier parameters as requested via the
1533
    disabled_barriers and disable_meta_flush arguments, and according to the
1534
    supported ones in the DRBD version vmaj.vmin.vrel
1535

1536
    If the desired option is unsupported, raises errors.BlockDeviceError.
1537

1538
    """
1539
    disabled_barriers_set = frozenset(disabled_barriers)
1540
    if not disabled_barriers_set in constants.DRBD_VALID_BARRIER_OPT:
1541
      raise errors.BlockDeviceError("%s is not a valid option set for DRBD"
1542
                                    " barriers" % disabled_barriers)
1543

    
1544
    args = []
1545

    
1546
    # The following code assumes DRBD 8.x, with x < 4 and x != 1 (DRBD 8.1.x
1547
    # does not exist)
1548
    if not vmaj == 8 and vmin in (0, 2, 3):
1549
      raise errors.BlockDeviceError("Unsupported DRBD version: %d.%d.%d" %
1550
                                    (vmaj, vmin, vrel))
1551

    
1552
    def _AppendOrRaise(option, min_version):
1553
      """Helper for DRBD options"""
1554
      if min_version is not None and vrel >= min_version:
1555
        args.append(option)
1556
      else:
1557
        raise errors.BlockDeviceError("Could not use the option %s as the"
1558
                                      " DRBD version %d.%d.%d does not support"
1559
                                      " it." % (option, vmaj, vmin, vrel))
1560

    
1561
    # the minimum version for each feature is encoded via pairs of (minor
1562
    # version -> x) where x is version in which support for the option was
1563
    # introduced.
1564
    meta_flush_supported = disk_flush_supported = {
1565
      0: 12,
1566
      2: 7,
1567
      3: 0,
1568
      }
1569

    
1570
    disk_drain_supported = {
1571
      2: 7,
1572
      3: 0,
1573
      }
1574

    
1575
    disk_barriers_supported = {
1576
      3: 0,
1577
      }
1578

    
1579
    # meta flushes
1580
    if disable_meta_flush:
1581
      _AppendOrRaise(cls._DISABLE_META_FLUSH_OPTION,
1582
                     meta_flush_supported.get(vmin, None))
1583

    
1584
    # disk flushes
1585
    if constants.DRBD_B_DISK_FLUSH in disabled_barriers_set:
1586
      _AppendOrRaise(cls._DISABLE_FLUSH_OPTION,
1587
                     disk_flush_supported.get(vmin, None))
1588

    
1589
    # disk drain
1590
    if constants.DRBD_B_DISK_DRAIN in disabled_barriers_set:
1591
      _AppendOrRaise(cls._DISABLE_DRAIN_OPTION,
1592
                     disk_drain_supported.get(vmin, None))
1593

    
1594
    # disk barriers
1595
    if constants.DRBD_B_DISK_BARRIERS in disabled_barriers_set:
1596
      _AppendOrRaise(cls._DISABLE_DISK_OPTION,
1597
                     disk_barriers_supported.get(vmin, None))
1598

    
1599
    return args
1600

    
1601
  def _AssembleNet(self, minor, net_info, protocol,
1602
                   dual_pri=False, hmac=None, secret=None):
1603
    """Configure the network part of the device.
1604

1605
    """
1606
    lhost, lport, rhost, rport = net_info
1607
    if None in net_info:
1608
      # we don't want network connection and actually want to make
1609
      # sure its shutdown
1610
      self._ShutdownNet(minor)
1611
      return
1612

    
1613
    # Workaround for a race condition. When DRBD is doing its dance to
1614
    # establish a connection with its peer, it also sends the
1615
    # synchronization speed over the wire. In some cases setting the
1616
    # sync speed only after setting up both sides can race with DRBD
1617
    # connecting, hence we set it here before telling DRBD anything
1618
    # about its peer.
1619
    sync_errors = self._SetMinorSyncParams(minor, self.params)
1620
    if sync_errors:
1621
      _ThrowError("drbd%d: can't set the synchronization parameters: %s" %
1622
                  (minor, utils.CommaJoin(sync_errors)))
1623

    
1624
    if netutils.IP6Address.IsValid(lhost):
1625
      if not netutils.IP6Address.IsValid(rhost):
1626
        _ThrowError("drbd%d: can't connect ip %s to ip %s" %
1627
                    (minor, lhost, rhost))
1628
      family = "ipv6"
1629
    elif netutils.IP4Address.IsValid(lhost):
1630
      if not netutils.IP4Address.IsValid(rhost):
1631
        _ThrowError("drbd%d: can't connect ip %s to ip %s" %
1632
                    (minor, lhost, rhost))
1633
      family = "ipv4"
1634
    else:
1635
      _ThrowError("drbd%d: Invalid ip %s" % (minor, lhost))
1636

    
1637
    args = ["drbdsetup", self._DevPath(minor), "net",
1638
            "%s:%s:%s" % (family, lhost, lport),
1639
            "%s:%s:%s" % (family, rhost, rport), protocol,
1640
            "-A", "discard-zero-changes",
1641
            "-B", "consensus",
1642
            "--create-device",
1643
            ]
1644
    if dual_pri:
1645
      args.append("-m")
1646
    if hmac and secret:
1647
      args.extend(["-a", hmac, "-x", secret])
1648

    
1649
    if self.params[constants.LDP_NET_CUSTOM]:
1650
      args.extend(shlex.split(self.params[constants.LDP_NET_CUSTOM]))
1651

    
1652
    result = utils.RunCmd(args)
1653
    if result.failed:
1654
      _ThrowError("drbd%d: can't setup network: %s - %s",
1655
                  minor, result.fail_reason, result.output)
1656

    
1657
    def _CheckNetworkConfig():
1658
      info = self._GetDevInfo(self._GetShowData(minor))
1659
      if not "local_addr" in info or not "remote_addr" in info:
1660
        raise utils.RetryAgain()
1661

    
1662
      if (info["local_addr"] != (lhost, lport) or
1663
          info["remote_addr"] != (rhost, rport)):
1664
        raise utils.RetryAgain()
1665

    
1666
    try:
1667
      utils.Retry(_CheckNetworkConfig, 1.0, 10.0)
1668
    except utils.RetryTimeout:
1669
      _ThrowError("drbd%d: timeout while configuring network", minor)
1670

    
1671
  def AddChildren(self, devices):
1672
    """Add a disk to the DRBD device.
1673

1674
    """
1675
    if self.minor is None:
1676
      _ThrowError("drbd%d: can't attach to dbrd8 during AddChildren",
1677
                  self._aminor)
1678
    if len(devices) != 2:
1679
      _ThrowError("drbd%d: need two devices for AddChildren", self.minor)
1680
    info = self._GetDevInfo(self._GetShowData(self.minor))
1681
    if "local_dev" in info:
1682
      _ThrowError("drbd%d: already attached to a local disk", self.minor)
1683
    backend, meta = devices
1684
    if backend.dev_path is None or meta.dev_path is None:
1685
      _ThrowError("drbd%d: children not ready during AddChildren", self.minor)
1686
    backend.Open()
1687
    meta.Open()
1688
    self._CheckMetaSize(meta.dev_path)
1689
    self._InitMeta(self._FindUnusedMinor(), meta.dev_path)
1690

    
1691
    self._AssembleLocal(self.minor, backend.dev_path, meta.dev_path, self.size)
1692
    self._children = devices
1693

    
1694
  def RemoveChildren(self, devices):
1695
    """Detach the drbd device from local storage.
1696

1697
    """
1698
    if self.minor is None:
1699
      _ThrowError("drbd%d: can't attach to drbd8 during RemoveChildren",
1700
                  self._aminor)
1701
    # early return if we don't actually have backing storage
1702
    info = self._GetDevInfo(self._GetShowData(self.minor))
1703
    if "local_dev" not in info:
1704
      return
1705
    if len(self._children) != 2:
1706
      _ThrowError("drbd%d: we don't have two children: %s", self.minor,
1707
                  self._children)
1708
    if self._children.count(None) == 2: # we don't actually have children :)
1709
      logging.warning("drbd%d: requested detach while detached", self.minor)
1710
      return
1711
    if len(devices) != 2:
1712
      _ThrowError("drbd%d: we need two children in RemoveChildren", self.minor)
1713
    for child, dev in zip(self._children, devices):
1714
      if dev != child.dev_path:
1715
        _ThrowError("drbd%d: mismatch in local storage (%s != %s) in"
1716
                    " RemoveChildren", self.minor, dev, child.dev_path)
1717

    
1718
    self._ShutdownLocal(self.minor)
1719
    self._children = []
1720

    
1721
  @classmethod
1722
  def _SetMinorSyncParams(cls, minor, params):
1723
    """Set the parameters of the DRBD syncer.
1724

1725
    This is the low-level implementation.
1726

1727
    @type minor: int
1728
    @param minor: the drbd minor whose settings we change
1729
    @type params: dict
1730
    @param params: LD level disk parameters related to the synchronization
1731
    @rtype: list
1732
    @return: a list of error messages
1733

1734
    """
1735

    
1736
    args = ["drbdsetup", cls._DevPath(minor), "syncer"]
1737
    if params[constants.LDP_DYNAMIC_RESYNC]:
1738
      version = cls._GetVersion(cls._GetProcData())
1739
      vmin = version["k_minor"]
1740
      vrel = version["k_point"]
1741

    
1742
      # By definition we are using 8.x, so just check the rest of the version
1743
      # number
1744
      if vmin != 3 or vrel < 9:
1745
        msg = ("The current DRBD version (8.%d.%d) does not support the "
1746
               "dynamic resync speed controller" % (vmin, vrel))
1747
        logging.error(msg)
1748
        return [msg]
1749

    
1750
      if params[constants.LDP_PLAN_AHEAD] == 0:
1751
        msg = ("A value of 0 for c-plan-ahead disables the dynamic sync speed"
1752
               " controller at DRBD level. If you want to disable it, please"
1753
               " set the dynamic-resync disk parameter to False.")
1754
        logging.error(msg)
1755
        return [msg]
1756

    
1757
      # add the c-* parameters to args
1758
      args.extend(["--c-plan-ahead", params[constants.LDP_PLAN_AHEAD],
1759
                   "--c-fill-target", params[constants.LDP_FILL_TARGET],
1760
                   "--c-delay-target", params[constants.LDP_DELAY_TARGET],
1761
                   "--c-max-rate", params[constants.LDP_MAX_RATE],
1762
                   "--c-min-rate", params[constants.LDP_MIN_RATE],
1763
                   ])
1764

    
1765
    else:
1766
      args.extend(["-r", "%d" % params[constants.LDP_RESYNC_RATE]])
1767

    
1768
    args.append("--create-device")
1769
    result = utils.RunCmd(args)
1770
    if result.failed:
1771
      msg = ("Can't change syncer rate: %s - %s" %
1772
             (result.fail_reason, result.output))
1773
      logging.error(msg)
1774
      return [msg]
1775

    
1776
    return []
1777

    
1778
  def SetSyncParams(self, params):
1779
    """Set the synchronization parameters of the DRBD syncer.
1780

1781
    @type params: dict
1782
    @param params: LD level disk parameters related to the synchronization
1783
    @rtype: list
1784
    @return: a list of error messages, emitted both by the current node and by
1785
    children. An empty list means no errors
1786

1787
    """
1788
    if self.minor is None:
1789
      err = "Not attached during SetSyncParams"
1790
      logging.info(err)
1791
      return [err]
1792

    
1793
    children_result = super(DRBD8, self).SetSyncParams(params)
1794
    children_result.extend(self._SetMinorSyncParams(self.minor, params))
1795
    return children_result
1796

    
1797
  def PauseResumeSync(self, pause):
1798
    """Pauses or resumes the sync of a DRBD device.
1799

1800
    @param pause: Wether to pause or resume
1801
    @return: the success of the operation
1802

1803
    """
1804
    if self.minor is None:
1805
      logging.info("Not attached during PauseSync")
1806
      return False
1807

    
1808
    children_result = super(DRBD8, self).PauseResumeSync(pause)
1809

    
1810
    if pause:
1811
      cmd = "pause-sync"
1812
    else:
1813
      cmd = "resume-sync"
1814

    
1815
    result = utils.RunCmd(["drbdsetup", self.dev_path, cmd])
1816
    if result.failed:
1817
      logging.error("Can't %s: %s - %s", cmd,
1818
                    result.fail_reason, result.output)
1819
    return not result.failed and children_result
1820

    
1821
  def GetProcStatus(self):
1822
    """Return device data from /proc.
1823

1824
    """
1825
    if self.minor is None:
1826
      _ThrowError("drbd%d: GetStats() called while not attached", self._aminor)
1827
    proc_info = self._MassageProcData(self._GetProcData())
1828
    if self.minor not in proc_info:
1829
      _ThrowError("drbd%d: can't find myself in /proc", self.minor)
1830
    return DRBD8Status(proc_info[self.minor])
1831

    
1832
  def GetSyncStatus(self):
1833
    """Returns the sync status of the device.
1834

1835

1836
    If sync_percent is None, it means all is ok
1837
    If estimated_time is None, it means we can't estimate
1838
    the time needed, otherwise it's the time left in seconds.
1839

1840

1841
    We set the is_degraded parameter to True on two conditions:
1842
    network not connected or local disk missing.
1843

1844
    We compute the ldisk parameter based on whether we have a local
1845
    disk or not.
1846

1847
    @rtype: objects.BlockDevStatus
1848

1849
    """
1850
    if self.minor is None and not self.Attach():
1851
      _ThrowError("drbd%d: can't Attach() in GetSyncStatus", self._aminor)
1852

    
1853
    stats = self.GetProcStatus()
1854
    is_degraded = not stats.is_connected or not stats.is_disk_uptodate
1855

    
1856
    if stats.is_disk_uptodate:
1857
      ldisk_status = constants.LDS_OKAY
1858
    elif stats.is_diskless:
1859
      ldisk_status = constants.LDS_FAULTY
1860
    else:
1861
      ldisk_status = constants.LDS_UNKNOWN
1862

    
1863
    return objects.BlockDevStatus(dev_path=self.dev_path,
1864
                                  major=self.major,
1865
                                  minor=self.minor,
1866
                                  sync_percent=stats.sync_percent,
1867
                                  estimated_time=stats.est_time,
1868
                                  is_degraded=is_degraded,
1869
                                  ldisk_status=ldisk_status)
1870

    
1871
  def Open(self, force=False):
1872
    """Make the local state primary.
1873

1874
    If the 'force' parameter is given, the '-o' option is passed to
1875
    drbdsetup. Since this is a potentially dangerous operation, the
1876
    force flag should be only given after creation, when it actually
1877
    is mandatory.
1878

1879
    """
1880
    if self.minor is None and not self.Attach():
1881
      logging.error("DRBD cannot attach to a device during open")
1882
      return False
1883
    cmd = ["drbdsetup", self.dev_path, "primary"]
1884
    if force:
1885
      cmd.append("-o")
1886
    result = utils.RunCmd(cmd)
1887
    if result.failed:
1888
      _ThrowError("drbd%d: can't make drbd device primary: %s", self.minor,
1889
                  result.output)
1890

    
1891
  def Close(self):
1892
    """Make the local state secondary.
1893

1894
    This will, of course, fail if the device is in use.
1895

1896
    """
1897
    if self.minor is None and not self.Attach():
1898
      _ThrowError("drbd%d: can't Attach() in Close()", self._aminor)
1899
    result = utils.RunCmd(["drbdsetup", self.dev_path, "secondary"])
1900
    if result.failed:
1901
      _ThrowError("drbd%d: can't switch drbd device to secondary: %s",
1902
                  self.minor, result.output)
1903

    
1904
  def DisconnectNet(self):
1905
    """Removes network configuration.
1906

1907
    This method shutdowns the network side of the device.
1908

1909
    The method will wait up to a hardcoded timeout for the device to
1910
    go into standalone after the 'disconnect' command before
1911
    re-configuring it, as sometimes it takes a while for the
1912
    disconnect to actually propagate and thus we might issue a 'net'
1913
    command while the device is still connected. If the device will
1914
    still be attached to the network and we time out, we raise an
1915
    exception.
1916

1917
    """
1918
    if self.minor is None:
1919
      _ThrowError("drbd%d: disk not attached in re-attach net", self._aminor)
1920

    
1921
    if None in (self._lhost, self._lport, self._rhost, self._rport):
1922
      _ThrowError("drbd%d: DRBD disk missing network info in"
1923
                  " DisconnectNet()", self.minor)
1924

    
1925
    class _DisconnectStatus:
1926
      def __init__(self, ever_disconnected):
1927
        self.ever_disconnected = ever_disconnected
1928

    
1929
    dstatus = _DisconnectStatus(_IgnoreError(self._ShutdownNet, self.minor))
1930

    
1931
    def _WaitForDisconnect():
1932
      if self.GetProcStatus().is_standalone:
1933
        return
1934

    
1935
      # retry the disconnect, it seems possible that due to a well-time
1936
      # disconnect on the peer, my disconnect command might be ignored and
1937
      # forgotten
1938
      dstatus.ever_disconnected = \
1939
        _IgnoreError(self._ShutdownNet, self.minor) or dstatus.ever_disconnected
1940

    
1941
      raise utils.RetryAgain()
1942

    
1943
    # Keep start time
1944
    start_time = time.time()
1945

    
1946
    try:
1947
      # Start delay at 100 milliseconds and grow up to 2 seconds
1948
      utils.Retry(_WaitForDisconnect, (0.1, 1.5, 2.0),
1949
                  self._NET_RECONFIG_TIMEOUT)
1950
    except utils.RetryTimeout:
1951
      if dstatus.ever_disconnected:
1952
        msg = ("drbd%d: device did not react to the"
1953
               " 'disconnect' command in a timely manner")
1954
      else:
1955
        msg = "drbd%d: can't shutdown network, even after multiple retries"
1956

    
1957
      _ThrowError(msg, self.minor)
1958

    
1959
    reconfig_time = time.time() - start_time
1960
    if reconfig_time > (self._NET_RECONFIG_TIMEOUT * 0.25):
1961
      logging.info("drbd%d: DisconnectNet: detach took %.3f seconds",
1962
                   self.minor, reconfig_time)
1963

    
1964
  def AttachNet(self, multimaster):
1965
    """Reconnects the network.
1966

1967
    This method connects the network side of the device with a
1968
    specified multi-master flag. The device needs to be 'Standalone'
1969
    but have valid network configuration data.
1970

1971
    Args:
1972
      - multimaster: init the network in dual-primary mode
1973

1974
    """
1975
    if self.minor is None:
1976
      _ThrowError("drbd%d: device not attached in AttachNet", self._aminor)
1977

    
1978
    if None in (self._lhost, self._lport, self._rhost, self._rport):
1979
      _ThrowError("drbd%d: missing network info in AttachNet()", self.minor)
1980

    
1981
    status = self.GetProcStatus()
1982

    
1983
    if not status.is_standalone:
1984
      _ThrowError("drbd%d: device is not standalone in AttachNet", self.minor)
1985

    
1986
    self._AssembleNet(self.minor,
1987
                      (self._lhost, self._lport, self._rhost, self._rport),
1988
                      constants.DRBD_NET_PROTOCOL, dual_pri=multimaster,
1989
                      hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1990

    
1991
  def Attach(self):
1992
    """Check if our minor is configured.
1993

1994
    This doesn't do any device configurations - it only checks if the
1995
    minor is in a state different from Unconfigured.
1996

1997
    Note that this function will not change the state of the system in
1998
    any way (except in case of side-effects caused by reading from
1999
    /proc).
2000

2001
    """
2002
    used_devs = self.GetUsedDevs()
2003
    if self._aminor in used_devs:
2004
      minor = self._aminor
2005
    else:
2006
      minor = None
2007

    
2008
    self._SetFromMinor(minor)
2009
    return minor is not None
2010

    
2011
  def Assemble(self):
2012
    """Assemble the drbd.
2013

2014
    Method:
2015
      - if we have a configured device, we try to ensure that it matches
2016
        our config
2017
      - if not, we create it from zero
2018
      - anyway, set the device parameters
2019

2020
    """
2021
    super(DRBD8, self).Assemble()
2022

    
2023
    self.Attach()
2024
    if self.minor is None:
2025
      # local device completely unconfigured
2026
      self._FastAssemble()
2027
    else:
2028
      # we have to recheck the local and network status and try to fix
2029
      # the device
2030
      self._SlowAssemble()
2031

    
2032
    sync_errors = self.SetSyncParams(self.params)
2033
    if sync_errors:
2034
      _ThrowError("drbd%d: can't set the synchronization parameters: %s" %
2035
                  (self.minor, utils.CommaJoin(sync_errors)))
2036

    
2037
  def _SlowAssemble(self):
2038
    """Assembles the DRBD device from a (partially) configured device.
2039

2040
    In case of partially attached (local device matches but no network
2041
    setup), we perform the network attach. If successful, we re-test
2042
    the attach if can return success.
2043

2044
    """
2045
    # TODO: Rewrite to not use a for loop just because there is 'break'
2046
    # pylint: disable=W0631
2047
    net_data = (self._lhost, self._lport, self._rhost, self._rport)
2048
    for minor in (self._aminor,):
2049
      info = self._GetDevInfo(self._GetShowData(minor))
2050
      match_l = self._MatchesLocal(info)
2051
      match_r = self._MatchesNet(info)
2052

    
2053
      if match_l and match_r:
2054
        # everything matches
2055
        break
2056

    
2057
      if match_l and not match_r and "local_addr" not in info:
2058
        # disk matches, but not attached to network, attach and recheck
2059
        self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
2060
                          hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
2061
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
2062
          break
2063
        else:
2064
          _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
2065
                      " show' disagrees", minor)
2066

    
2067
      if match_r and "local_dev" not in info:
2068
        # no local disk, but network attached and it matches
2069
        self._AssembleLocal(minor, self._children[0].dev_path,
2070
                            self._children[1].dev_path, self.size)
2071
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
2072
          break
2073
        else:
2074
          _ThrowError("drbd%d: disk attach successful, but 'drbdsetup"
2075
                      " show' disagrees", minor)
2076

    
2077
      # this case must be considered only if we actually have local
2078
      # storage, i.e. not in diskless mode, because all diskless
2079
      # devices are equal from the point of view of local
2080
      # configuration
2081
      if (match_l and "local_dev" in info and
2082
          not match_r and "local_addr" in info):
2083
        # strange case - the device network part points to somewhere
2084
        # else, even though its local storage is ours; as we own the
2085
        # drbd space, we try to disconnect from the remote peer and
2086
        # reconnect to our correct one
2087
        try:
2088
          self._ShutdownNet(minor)
2089
        except errors.BlockDeviceError, err:
2090
          _ThrowError("drbd%d: device has correct local storage, wrong"
2091
                      " remote peer and is unable to disconnect in order"
2092
                      " to attach to the correct peer: %s", minor, str(err))
2093
        # note: _AssembleNet also handles the case when we don't want
2094
        # local storage (i.e. one or more of the _[lr](host|port) is
2095
        # None)
2096
        self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
2097
                          hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
2098
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
2099
          break
2100
        else:
2101
          _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
2102
                      " show' disagrees", minor)
2103

    
2104
    else:
2105
      minor = None
2106

    
2107
    self._SetFromMinor(minor)
2108
    if minor is None:
2109
      _ThrowError("drbd%d: cannot activate, unknown or unhandled reason",
2110
                  self._aminor)
2111

    
2112
  def _FastAssemble(self):
2113
    """Assemble the drbd device from zero.
2114

2115
    This is run when in Assemble we detect our minor is unused.
2116

2117
    """
2118
    minor = self._aminor
2119
    if self._children and self._children[0] and self._children[1]:
2120
      self._AssembleLocal(minor, self._children[0].dev_path,
2121
                          self._children[1].dev_path, self.size)
2122
    if self._lhost and self._lport and self._rhost and self._rport:
2123
      self._AssembleNet(minor,
2124
                        (self._lhost, self._lport, self._rhost, self._rport),
2125
                        constants.DRBD_NET_PROTOCOL,
2126
                        hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
2127
    self._SetFromMinor(minor)
2128

    
2129
  @classmethod
2130
  def _ShutdownLocal(cls, minor):
2131
    """Detach from the local device.
2132

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

2136
    """
2137
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "detach"])
2138
    if result.failed:
2139
      _ThrowError("drbd%d: can't detach local disk: %s", minor, result.output)
2140

    
2141
  @classmethod
2142
  def _ShutdownNet(cls, minor):
2143
    """Disconnect from the remote peer.
2144

2145
    This fails if we don't have a local device.
2146

2147
    """
2148
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disconnect"])
2149
    if result.failed:
2150
      _ThrowError("drbd%d: can't shutdown network: %s", minor, result.output)
2151

    
2152
  @classmethod
2153
  def _ShutdownAll(cls, minor):
2154
    """Deactivate the device.
2155

2156
    This will, of course, fail if the device is in use.
2157

2158
    """
2159
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "down"])
2160
    if result.failed:
2161
      _ThrowError("drbd%d: can't shutdown drbd device: %s",
2162
                  minor, result.output)
2163

    
2164
  def Shutdown(self):
2165
    """Shutdown the DRBD device.
2166

2167
    """
2168
    if self.minor is None and not self.Attach():
2169
      logging.info("drbd%d: not attached during Shutdown()", self._aminor)
2170
      return
2171
    minor = self.minor
2172
    self.minor = None
2173
    self.dev_path = None
2174
    self._ShutdownAll(minor)
2175

    
2176
  def Remove(self):
2177
    """Stub remove for DRBD devices.
2178

2179
    """
2180
    self.Shutdown()
2181

    
2182
  @classmethod
2183
  def Create(cls, unique_id, children, size, params):
2184
    """Create a new DRBD8 device.
2185

2186
    Since DRBD devices are not created per se, just assembled, this
2187
    function only initializes the metadata.
2188

2189
    """
2190
    if len(children) != 2:
2191
      raise errors.ProgrammerError("Invalid setup for the drbd device")
2192
    # check that the minor is unused
2193
    aminor = unique_id[4]
2194
    proc_info = cls._MassageProcData(cls._GetProcData())
2195
    if aminor in proc_info:
2196
      status = DRBD8Status(proc_info[aminor])
2197
      in_use = status.is_in_use
2198
    else:
2199
      in_use = False
2200
    if in_use:
2201
      _ThrowError("drbd%d: minor is already in use at Create() time", aminor)
2202
    meta = children[1]
2203
    meta.Assemble()
2204
    if not meta.Attach():
2205
      _ThrowError("drbd%d: can't attach to meta device '%s'",
2206
                  aminor, meta)
2207
    cls._CheckMetaSize(meta.dev_path)
2208
    cls._InitMeta(aminor, meta.dev_path)
2209
    return cls(unique_id, children, size, params)
2210

    
2211
  def Grow(self, amount, dryrun, backingstore):
2212
    """Resize the DRBD device and its backing storage.
2213

2214
    """
2215
    if self.minor is None:
2216
      _ThrowError("drbd%d: Grow called while not attached", self._aminor)
2217
    if len(self._children) != 2 or None in self._children:
2218
      _ThrowError("drbd%d: cannot grow diskless device", self.minor)
2219
    self._children[0].Grow(amount, dryrun, backingstore)
2220
    if dryrun or backingstore:
2221
      # DRBD does not support dry-run mode and is not backing storage,
2222
      # so we'll return here
2223
      return
2224
    result = utils.RunCmd(["drbdsetup", self.dev_path, "resize", "-s",
2225
                           "%dm" % (self.size + amount)])
2226
    if result.failed:
2227
      _ThrowError("drbd%d: resize failed: %s", self.minor, result.output)
2228

    
2229

    
2230
class FileStorage(BlockDev):
2231
  """File device.
2232

2233
  This class represents the a file storage backend device.
2234

2235
  The unique_id for the file device is a (file_driver, file_path) tuple.
2236

2237
  """
2238
  def __init__(self, unique_id, children, size, params):
2239
    """Initalizes a file device backend.
2240

2241
    """
2242
    if children:
2243
      raise errors.BlockDeviceError("Invalid setup for file device")
2244
    super(FileStorage, self).__init__(unique_id, children, size, params)
2245
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
2246
      raise ValueError("Invalid configuration data %s" % str(unique_id))
2247
    self.driver = unique_id[0]
2248
    self.dev_path = unique_id[1]
2249
    self.Attach()
2250

    
2251
  def Assemble(self):
2252
    """Assemble the device.
2253

2254
    Checks whether the file device exists, raises BlockDeviceError otherwise.
2255

2256
    """
2257
    if not os.path.exists(self.dev_path):
2258
      _ThrowError("File device '%s' does not exist" % self.dev_path)
2259

    
2260
  def Shutdown(self):
2261
    """Shutdown the device.
2262

2263
    This is a no-op for the file type, as we don't deactivate
2264
    the file on shutdown.
2265

2266
    """
2267
    pass
2268

    
2269
  def Open(self, force=False):
2270
    """Make the device ready for I/O.
2271

2272
    This is a no-op for the file type.
2273

2274
    """
2275
    pass
2276

    
2277
  def Close(self):
2278
    """Notifies that the device will no longer be used for I/O.
2279

2280
    This is a no-op for the file type.
2281

2282
    """
2283
    pass
2284

    
2285
  def Remove(self):
2286
    """Remove the file backing the block device.
2287

2288
    @rtype: boolean
2289
    @return: True if the removal was successful
2290

2291
    """
2292
    try:
2293
      os.remove(self.dev_path)
2294
    except OSError, err:
2295
      if err.errno != errno.ENOENT:
2296
        _ThrowError("Can't remove file '%s': %s", self.dev_path, err)
2297

    
2298
  def Rename(self, new_id):
2299
    """Renames the file.
2300

2301
    """
2302
    # TODO: implement rename for file-based storage
2303
    _ThrowError("Rename is not supported for file-based storage")
2304

    
2305
  def Grow(self, amount, dryrun, backingstore):
2306
    """Grow the file
2307

2308
    @param amount: the amount (in mebibytes) to grow with
2309

2310
    """
2311
    if not backingstore:
2312
      return
2313
    # Check that the file exists
2314
    self.Assemble()
2315
    current_size = self.GetActualSize()
2316
    new_size = current_size + amount * 1024 * 1024
2317
    assert new_size > current_size, "Cannot Grow with a negative amount"
2318
    # We can't really simulate the growth
2319
    if dryrun:
2320
      return
2321
    try:
2322
      f = open(self.dev_path, "a+")
2323
      f.truncate(new_size)
2324
      f.close()
2325
    except EnvironmentError, err:
2326
      _ThrowError("Error in file growth: %", str(err))
2327

    
2328
  def Attach(self):
2329
    """Attach to an existing file.
2330

2331
    Check if this file already exists.
2332

2333
    @rtype: boolean
2334
    @return: True if file exists
2335

2336
    """
2337
    self.attached = os.path.exists(self.dev_path)
2338
    return self.attached
2339

    
2340
  def GetActualSize(self):
2341
    """Return the actual disk size.
2342

2343
    @note: the device needs to be active when this is called
2344

2345
    """
2346
    assert self.attached, "BlockDevice not attached in GetActualSize()"
2347
    try:
2348
      st = os.stat(self.dev_path)
2349
      return st.st_size
2350
    except OSError, err:
2351
      _ThrowError("Can't stat %s: %s", self.dev_path, err)
2352

    
2353
  @classmethod
2354
  def Create(cls, unique_id, children, size, params):
2355
    """Create a new file.
2356

2357
    @param size: the size of file in MiB
2358

2359
    @rtype: L{bdev.FileStorage}
2360
    @return: an instance of FileStorage
2361

2362
    """
2363
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
2364
      raise ValueError("Invalid configuration data %s" % str(unique_id))
2365
    dev_path = unique_id[1]
2366
    try:
2367
      fd = os.open(dev_path, os.O_RDWR | os.O_CREAT | os.O_EXCL)
2368
      f = os.fdopen(fd, "w")
2369
      f.truncate(size * 1024 * 1024)
2370
      f.close()
2371
    except EnvironmentError, err:
2372
      if err.errno == errno.EEXIST:
2373
        _ThrowError("File already existing: %s", dev_path)
2374
      _ThrowError("Error in file creation: %", str(err))
2375

    
2376
    return FileStorage(unique_id, children, size, params)
2377

    
2378

    
2379
class PersistentBlockDevice(BlockDev):
2380
  """A block device with persistent node
2381

2382
  May be either directly attached, or exposed through DM (e.g. dm-multipath).
2383
  udev helpers are probably required to give persistent, human-friendly
2384
  names.
2385

2386
  For the time being, pathnames are required to lie under /dev.
2387

2388
  """
2389
  def __init__(self, unique_id, children, size, params):
2390
    """Attaches to a static block device.
2391

2392
    The unique_id is a path under /dev.
2393

2394
    """
2395
    super(PersistentBlockDevice, self).__init__(unique_id, children, size,
2396
                                                params)
2397
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
2398
      raise ValueError("Invalid configuration data %s" % str(unique_id))
2399
    self.dev_path = unique_id[1]
2400
    if not os.path.realpath(self.dev_path).startswith("/dev/"):
2401
      raise ValueError("Full path '%s' lies outside /dev" %
2402
                              os.path.realpath(self.dev_path))
2403
    # TODO: this is just a safety guard checking that we only deal with devices
2404
    # we know how to handle. In the future this will be integrated with
2405
    # external storage backends and possible values will probably be collected
2406
    # from the cluster configuration.
2407
    if unique_id[0] != constants.BLOCKDEV_DRIVER_MANUAL:
2408
      raise ValueError("Got persistent block device of invalid type: %s" %
2409
                       unique_id[0])
2410

    
2411
    self.major = self.minor = None
2412
    self.Attach()
2413

    
2414
  @classmethod
2415
  def Create(cls, unique_id, children, size, params):
2416
    """Create a new device
2417

2418
    This is a noop, we only return a PersistentBlockDevice instance
2419

2420
    """
2421
    return PersistentBlockDevice(unique_id, children, 0, params)
2422

    
2423
  def Remove(self):
2424
    """Remove a device
2425

2426
    This is a noop
2427

2428
    """
2429
    pass
2430

    
2431
  def Rename(self, new_id):
2432
    """Rename this device.
2433

2434
    """
2435
    _ThrowError("Rename is not supported for PersistentBlockDev storage")
2436

    
2437
  def Attach(self):
2438
    """Attach to an existing block device.
2439

2440

2441
    """
2442
    self.attached = False
2443
    try:
2444
      st = os.stat(self.dev_path)
2445
    except OSError, err:
2446
      logging.error("Error stat()'ing %s: %s", self.dev_path, str(err))
2447
      return False
2448

    
2449
    if not stat.S_ISBLK(st.st_mode):
2450
      logging.error("%s is not a block device", self.dev_path)
2451
      return False
2452

    
2453
    self.major = os.major(st.st_rdev)
2454
    self.minor = os.minor(st.st_rdev)
2455
    self.attached = True
2456

    
2457
    return True
2458

    
2459
  def Assemble(self):
2460
    """Assemble the device.
2461

2462
    """
2463
    pass
2464

    
2465
  def Shutdown(self):
2466
    """Shutdown the device.
2467

2468
    """
2469
    pass
2470

    
2471
  def Open(self, force=False):
2472
    """Make the device ready for I/O.
2473

2474
    """
2475
    pass
2476

    
2477
  def Close(self):
2478
    """Notifies that the device will no longer be used for I/O.
2479

2480
    """
2481
    pass
2482

    
2483
  def Grow(self, amount, dryrun, backingstore):
2484
    """Grow the logical volume.
2485

2486
    """
2487
    _ThrowError("Grow is not supported for PersistentBlockDev storage")
2488

    
2489

    
2490
class RADOSBlockDevice(BlockDev):
2491
  """A RADOS Block Device (rbd).
2492

2493
  This class implements the RADOS Block Device for the backend. You need
2494
  the rbd kernel driver, the RADOS Tools and a working RADOS cluster for
2495
  this to be functional.
2496

2497
  """
2498
  def __init__(self, unique_id, children, size, params):
2499
    """Attaches to an rbd device.
2500

2501
    """
2502
    super(RADOSBlockDevice, self).__init__(unique_id, children, size, params)
2503
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
2504
      raise ValueError("Invalid configuration data %s" % str(unique_id))
2505

    
2506
    self.driver, self.rbd_name = unique_id
2507

    
2508
    self.major = self.minor = None
2509
    self.Attach()
2510

    
2511
  @classmethod
2512
  def Create(cls, unique_id, children, size, params):
2513
    """Create a new rbd device.
2514

2515
    Provision a new rbd volume inside a RADOS pool.
2516

2517
    """
2518
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
2519
      raise errors.ProgrammerError("Invalid configuration data %s" %
2520
                                   str(unique_id))
2521
    rbd_pool = params[constants.LDP_POOL]
2522
    rbd_name = unique_id[1]
2523

    
2524
    # Provision a new rbd volume (Image) inside the RADOS cluster.
2525
    cmd = [constants.RBD_CMD, "create", "-p", rbd_pool,
2526
           rbd_name, "--size", "%s" % size]
2527
    result = utils.RunCmd(cmd)
2528
    if result.failed:
2529
      _ThrowError("rbd creation failed (%s): %s",
2530
                  result.fail_reason, result.output)
2531

    
2532
    return RADOSBlockDevice(unique_id, children, size, params)
2533

    
2534
  def Remove(self):
2535
    """Remove the rbd device.
2536

2537
    """
2538
    rbd_pool = self.params[constants.LDP_POOL]
2539
    rbd_name = self.unique_id[1]
2540

    
2541
    if not self.minor and not self.Attach():
2542
      # The rbd device doesn't exist.
2543
      return
2544

    
2545
    # First shutdown the device (remove mappings).
2546
    self.Shutdown()
2547

    
2548
    # Remove the actual Volume (Image) from the RADOS cluster.
2549
    cmd = [constants.RBD_CMD, "rm", "-p", rbd_pool, rbd_name]
2550
    result = utils.RunCmd(cmd)
2551
    if result.failed:
2552
      _ThrowError("Can't remove Volume from cluster with rbd rm: %s - %s",
2553
                  result.fail_reason, result.output)
2554

    
2555
  def Rename(self, new_id):
2556
    """Rename this device.
2557

2558
    """
2559
    pass
2560

    
2561
  def Attach(self):
2562
    """Attach to an existing rbd device.
2563

2564
    This method maps the rbd volume that matches our name with
2565
    an rbd device and then attaches to this device.
2566

2567
    """
2568
    self.attached = False
2569

    
2570
    # Map the rbd volume to a block device under /dev
2571
    self.dev_path = self._MapVolumeToBlockdev(self.unique_id)
2572

    
2573
    try:
2574
      st = os.stat(self.dev_path)
2575
    except OSError, err:
2576
      logging.error("Error stat()'ing %s: %s", self.dev_path, str(err))
2577
      return False
2578

    
2579
    if not stat.S_ISBLK(st.st_mode):
2580
      logging.error("%s is not a block device", self.dev_path)
2581
      return False
2582

    
2583
    self.major = os.major(st.st_rdev)
2584
    self.minor = os.minor(st.st_rdev)
2585
    self.attached = True
2586

    
2587
    return True
2588

    
2589
  def _MapVolumeToBlockdev(self, unique_id):
2590
    """Maps existing rbd volumes to block devices.
2591

2592
    This method should be idempotent if the mapping already exists.
2593

2594
    @rtype: string
2595
    @return: the block device path that corresponds to the volume
2596

2597
    """
2598
    pool = self.params[constants.LDP_POOL]
2599
    name = unique_id[1]
2600

    
2601
    # Check if the mapping already exists.
2602
    showmap_cmd = [constants.RBD_CMD, "showmapped", "-p", pool]
2603
    result = utils.RunCmd(showmap_cmd)
2604
    if result.failed:
2605
      _ThrowError("rbd showmapped failed (%s): %s",
2606
                  result.fail_reason, result.output)
2607

    
2608
    rbd_dev = self._ParseRbdShowmappedOutput(result.output, name)
2609

    
2610
    if rbd_dev:
2611
      # The mapping exists. Return it.
2612
      return rbd_dev
2613

    
2614
    # The mapping doesn't exist. Create it.
2615
    map_cmd = [constants.RBD_CMD, "map", "-p", pool, name]
2616
    result = utils.RunCmd(map_cmd)
2617
    if result.failed:
2618
      _ThrowError("rbd map failed (%s): %s",
2619
                  result.fail_reason, result.output)
2620

    
2621
    # Find the corresponding rbd device.
2622
    showmap_cmd = [constants.RBD_CMD, "showmapped", "-p", pool]
2623
    result = utils.RunCmd(showmap_cmd)
2624
    if result.failed:
2625
      _ThrowError("rbd map succeeded, but showmapped failed (%s): %s",
2626
                  result.fail_reason, result.output)
2627

    
2628
    rbd_dev = self._ParseRbdShowmappedOutput(result.output, name)
2629

    
2630
    if not rbd_dev:
2631
      _ThrowError("rbd map succeeded, but could not find the rbd block"
2632
                  " device in output of showmapped, for volume: %s", name)
2633

    
2634
    # The device was successfully mapped. Return it.
2635
    return rbd_dev
2636

    
2637
  @staticmethod
2638
  def _ParseRbdShowmappedOutput(output, volume_name):
2639
    """Parse the output of `rbd showmapped'.
2640

2641
    This method parses the output of `rbd showmapped' and returns
2642
    the rbd block device path (e.g. /dev/rbd0) that matches the
2643
    given rbd volume.
2644

2645
    @type output: string
2646
    @param output: the whole output of `rbd showmapped'
2647
    @type volume_name: string
2648
    @param volume_name: the name of the volume whose device we search for
2649
    @rtype: string or None
2650
    @return: block device path if the volume is mapped, else None
2651

2652
    """
2653
    allfields = 5
2654
    volumefield = 2
2655
    devicefield = 4
2656

    
2657
    field_sep = "\t"
2658

    
2659
    lines = output.splitlines()
2660
    splitted_lines = map(lambda l: l.split(field_sep), lines)
2661

    
2662
    # Check empty output.
2663
    if not splitted_lines:
2664
      _ThrowError("rbd showmapped returned empty output")
2665

    
2666
    # Check showmapped header line, to determine number of fields.
2667
    field_cnt = len(splitted_lines[0])
2668
    if field_cnt != allfields:
2669
      _ThrowError("Cannot parse rbd showmapped output because its format"
2670
                  " seems to have changed; expected %s fields, found %s",
2671
                  allfields, field_cnt)
2672

    
2673
    matched_lines = \
2674
      filter(lambda l: len(l) == allfields and l[volumefield] == volume_name,
2675
             splitted_lines)
2676

    
2677
    if len(matched_lines) > 1:
2678
      _ThrowError("The rbd volume %s is mapped more than once."
2679
                  " This shouldn't happen, try to unmap the extra"
2680
                  " devices manually.", volume_name)
2681

    
2682
    if matched_lines:
2683
      # rbd block device found. Return it.
2684
      rbd_dev = matched_lines[0][devicefield]
2685
      return rbd_dev
2686

    
2687
    # The given volume is not mapped.
2688
    return None
2689

    
2690
  def Assemble(self):
2691
    """Assemble the device.
2692

2693
    """
2694
    pass
2695

    
2696
  def Shutdown(self):
2697
    """Shutdown the device.
2698

2699
    """
2700
    if not self.minor and not self.Attach():
2701
      # The rbd device doesn't exist.
2702
      return
2703

    
2704
    # Unmap the block device from the Volume.
2705
    self._UnmapVolumeFromBlockdev(self.unique_id)
2706

    
2707
    self.minor = None
2708
    self.dev_path = None
2709

    
2710
  def _UnmapVolumeFromBlockdev(self, unique_id):
2711
    """Unmaps the rbd device from the Volume it is mapped.
2712

2713
    Unmaps the rbd device from the Volume it was previously mapped to.
2714
    This method should be idempotent if the Volume isn't mapped.
2715

2716
    """
2717
    pool = self.params[constants.LDP_POOL]
2718
    name = unique_id[1]
2719

    
2720
    # Check if the mapping already exists.
2721
    showmap_cmd = [constants.RBD_CMD, "showmapped", "-p", pool]
2722
    result = utils.RunCmd(showmap_cmd)
2723
    if result.failed:
2724
      _ThrowError("rbd showmapped failed [during unmap](%s): %s",
2725
                  result.fail_reason, result.output)
2726

    
2727
    rbd_dev = self._ParseRbdShowmappedOutput(result.output, name)
2728

    
2729
    if rbd_dev:
2730
      # The mapping exists. Unmap the rbd device.
2731
      unmap_cmd = [constants.RBD_CMD, "unmap", "%s" % rbd_dev]
2732
      result = utils.RunCmd(unmap_cmd)
2733
      if result.failed:
2734
        _ThrowError("rbd unmap failed (%s): %s",
2735
                    result.fail_reason, result.output)
2736

    
2737
  def Open(self, force=False):
2738
    """Make the device ready for I/O.
2739

2740
    """
2741
    pass
2742

    
2743
  def Close(self):
2744
    """Notifies that the device will no longer be used for I/O.
2745

2746
    """
2747
    pass
2748

    
2749
  def Grow(self, amount, dryrun, backingstore):
2750
    """Grow the Volume.
2751

2752
    @type amount: integer
2753
    @param amount: the amount (in mebibytes) to grow with
2754
    @type dryrun: boolean
2755
    @param dryrun: whether to execute the operation in simulation mode
2756
        only, without actually increasing the size
2757

2758
    """
2759
    if not backingstore:
2760
      return
2761
    if not self.Attach():
2762
      _ThrowError("Can't attach to rbd device during Grow()")
2763

    
2764
    if dryrun:
2765
      # the rbd tool does not support dry runs of resize operations.
2766
      # Since rbd volumes are thinly provisioned, we assume
2767
      # there is always enough free space for the operation.
2768
      return
2769

    
2770
    rbd_pool = self.params[constants.LDP_POOL]
2771
    rbd_name = self.unique_id[1]
2772
    new_size = self.size + amount
2773

    
2774
    # Resize the rbd volume (Image) inside the RADOS cluster.
2775
    cmd = [constants.RBD_CMD, "resize", "-p", rbd_pool,
2776
           rbd_name, "--size", "%s" % new_size]
2777
    result = utils.RunCmd(cmd)
2778
    if result.failed:
2779
      _ThrowError("rbd resize failed (%s): %s",
2780
                  result.fail_reason, result.output)
2781

    
2782

    
2783
DEV_MAP = {
2784
  constants.LD_LV: LogicalVolume,
2785
  constants.LD_DRBD8: DRBD8,
2786
  constants.LD_BLOCKDEV: PersistentBlockDevice,
2787
  constants.LD_RBD: RADOSBlockDevice,
2788
  }
2789

    
2790
if constants.ENABLE_FILE_STORAGE or constants.ENABLE_SHARED_FILE_STORAGE:
2791
  DEV_MAP[constants.LD_FILE] = FileStorage
2792

    
2793

    
2794
def _VerifyDiskType(dev_type):
2795
  if dev_type not in DEV_MAP:
2796
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
2797

    
2798

    
2799
def _VerifyDiskParams(disk):
2800
  """Verifies if all disk parameters are set.
2801

2802
  """
2803
  missing = set(constants.DISK_LD_DEFAULTS[disk.dev_type]) - set(disk.params)
2804
  if missing:
2805
    raise errors.ProgrammerError("Block device is missing disk parameters: %s" %
2806
                                 missing)
2807

    
2808

    
2809
def FindDevice(disk, children):
2810
  """Search for an existing, assembled device.
2811

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

2815
  @type disk: L{objects.Disk}
2816
  @param disk: the disk object to find
2817
  @type children: list of L{bdev.BlockDev}
2818
  @param children: the list of block devices that are children of the device
2819
                  represented by the disk parameter
2820

2821
  """
2822
  _VerifyDiskType(disk.dev_type)
2823
  device = DEV_MAP[disk.dev_type](disk.physical_id, children, disk.size,
2824
                                  disk.params)
2825
  if not device.attached:
2826
    return None
2827
  return device
2828

    
2829

    
2830
def Assemble(disk, children):
2831
  """Try to attach or assemble an existing device.
2832

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

2836
  @type disk: L{objects.Disk}
2837
  @param disk: the disk object to assemble
2838
  @type children: list of L{bdev.BlockDev}
2839
  @param children: the list of block devices that are children of the device
2840
                  represented by the disk parameter
2841

2842
  """
2843
  _VerifyDiskType(disk.dev_type)
2844
  _VerifyDiskParams(disk)
2845
  device = DEV_MAP[disk.dev_type](disk.physical_id, children, disk.size,
2846
                                  disk.params)
2847
  device.Assemble()
2848
  return device
2849

    
2850

    
2851
def Create(disk, children):
2852
  """Create a device.
2853

2854
  @type disk: L{objects.Disk}
2855
  @param disk: the disk object to create
2856
  @type children: list of L{bdev.BlockDev}
2857
  @param children: the list of block devices that are children of the device
2858
                  represented by the disk parameter
2859

2860
  """
2861
  _VerifyDiskType(disk.dev_type)
2862
  _VerifyDiskParams(disk)
2863
  device = DEV_MAP[disk.dev_type].Create(disk.physical_id, children, disk.size,
2864
                                         disk.params)
2865
  return device