Statistics
| Branch: | Tag: | Revision:

root / lib / bdev.py @ fbdac0d9

History | View | Annotate | Download (88.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

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

    
41

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

    
45

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

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

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

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

    
63

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

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

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

    
77

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

81
  This tries to read the first 128k of the device.
82

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

    
91

    
92
def _CheckFileStoragePath(path, allowed):
93
  """Checks if a path is in a list of allowed paths for file storage.
94

95
  @type path: string
96
  @param path: Path to check
97
  @type allowed: list
98
  @param allowed: List of allowed paths
99
  @raise errors.FileStoragePathError: If the path is not allowed
100

101
  """
102
  if not os.path.isabs(path):
103
    raise errors.FileStoragePathError("File storage path must be absolute,"
104
                                      " got '%s'" % path)
105

    
106
  for i in allowed:
107
    if not os.path.isabs(i):
108
      logging.info("Ignoring relative path '%s' for file storage", i)
109
      continue
110

    
111
    if utils.IsBelowDir(i, path):
112
      break
113
  else:
114
    raise errors.FileStoragePathError("Path '%s' is not acceptable for file"
115
                                      " storage" % path)
116

    
117

    
118
def LoadAllowedFileStoragePaths(filename):
119
  """Loads file containing allowed file storage paths.
120

121
  @rtype: list
122
  @return: List of allowed paths (can be an empty list)
123

124
  """
125
  try:
126
    contents = utils.ReadFile(filename)
127
  except EnvironmentError:
128
    return []
129
  else:
130
    return utils.FilterEmptyLinesAndComments(contents)
131

    
132

    
133
def CheckFileStoragePath(path, _filename=pathutils.FILE_STORAGE_PATHS_FILE):
134
  """Checks if a path is allowed for file storage.
135

136
  @type path: string
137
  @param path: Path to check
138
  @raise errors.FileStoragePathError: If the path is not allowed
139

140
  """
141
  _CheckFileStoragePath(path, LoadAllowedFileStoragePaths(_filename))
142

    
143

    
144

    
145
class BlockDev(object):
146
  """Block device abstract class.
147

148
  A block device can be in the following states:
149
    - not existing on the system, and by `Create()` it goes into:
150
    - existing but not setup/not active, and by `Assemble()` goes into:
151
    - active read-write and by `Open()` it goes into
152
    - online (=used, or ready for use)
153

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

160
  The many different states of the device are due to the fact that we
161
  need to cover many device types:
162
    - logical volumes are created, lvchange -a y $lv, and used
163
    - drbd devices are attached to a local disk/remote peer and made primary
164

165
  A block device is identified by three items:
166
    - the /dev path of the device (dynamic)
167
    - a unique ID of the device (static)
168
    - it's major/minor pair (dynamic)
169

170
  Not all devices implement both the first two as distinct items. LVM
171
  logical volumes have their unique ID (the pair volume group, logical
172
  volume name) in a 1-to-1 relation to the dev path. For DRBD devices,
173
  the /dev path is again dynamic and the unique id is the pair (host1,
174
  dev1), (host2, dev2).
175

176
  You can get to a device in two ways:
177
    - creating the (real) device, which returns you
178
      an attached instance (lvcreate)
179
    - attaching of a python instance to an existing (real) device
180

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

187
  """
188
  def __init__(self, unique_id, children, size, params):
189
    self._children = children
190
    self.dev_path = None
191
    self.unique_id = unique_id
192
    self.major = None
193
    self.minor = None
194
    self.attached = False
195
    self.size = size
196
    self.params = params
197

    
198
  def Assemble(self):
199
    """Assemble the device from its components.
200

201
    Implementations of this method by child classes must ensure that:
202
      - after the device has been assembled, it knows its major/minor
203
        numbers; this allows other devices (usually parents) to probe
204
        correctly for their children
205
      - calling this method on an existing, in-use device is safe
206
      - if the device is already configured (and in an OK state),
207
        this method is idempotent
208

209
    """
210
    pass
211

    
212
  def Attach(self):
213
    """Find a device which matches our config and attach to it.
214

215
    """
216
    raise NotImplementedError
217

    
218
  def Close(self):
219
    """Notifies that the device will no longer be used for I/O.
220

221
    """
222
    raise NotImplementedError
223

    
224
  @classmethod
225
  def Create(cls, unique_id, children, size, params):
226
    """Create the device.
227

228
    If the device cannot be created, it will return None
229
    instead. Error messages go to the logging system.
230

231
    Note that for some devices, the unique_id is used, and for other,
232
    the children. The idea is that these two, taken together, are
233
    enough for both creation and assembly (later).
234

235
    """
236
    raise NotImplementedError
237

    
238
  def Remove(self):
239
    """Remove this device.
240

241
    This makes sense only for some of the device types: LV and file
242
    storage. Also note that if the device can't attach, the removal
243
    can't be completed.
244

245
    """
246
    raise NotImplementedError
247

    
248
  def Rename(self, new_id):
249
    """Rename this device.
250

251
    This may or may not make sense for a given device type.
252

253
    """
254
    raise NotImplementedError
255

    
256
  def Open(self, force=False):
257
    """Make the device ready for use.
258

259
    This makes the device ready for I/O. For now, just the DRBD
260
    devices need this.
261

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

265
    """
266
    raise NotImplementedError
267

    
268
  def Shutdown(self):
269
    """Shut down the device, freeing its children.
270

271
    This undoes the `Assemble()` work, except for the child
272
    assembling; as such, the children on the device are still
273
    assembled after this call.
274

275
    """
276
    raise NotImplementedError
277

    
278
  def SetSyncParams(self, params):
279
    """Adjust the synchronization parameters of the mirror.
280

281
    In case this is not a mirroring device, this is no-op.
282

283
    @param params: dictionary of LD level disk parameters related to the
284
    synchronization.
285
    @rtype: list
286
    @return: a list of error messages, emitted both by the current node and by
287
    children. An empty list means no errors.
288

289
    """
290
    result = []
291
    if self._children:
292
      for child in self._children:
293
        result.extend(child.SetSyncParams(params))
294
    return result
295

    
296
  def PauseResumeSync(self, pause):
297
    """Pause/Resume the sync of the mirror.
298

299
    In case this is not a mirroring device, this is no-op.
300

301
    @param pause: Whether to pause or resume
302

303
    """
304
    result = True
305
    if self._children:
306
      for child in self._children:
307
        result = result and child.PauseResumeSync(pause)
308
    return result
309

    
310
  def GetSyncStatus(self):
311
    """Returns the sync status of the device.
312

313
    If this device is a mirroring device, this function returns the
314
    status of the mirror.
315

316
    If sync_percent is None, it means the device is not syncing.
317

318
    If estimated_time is None, it means we can't estimate
319
    the time needed, otherwise it's the time left in seconds.
320

321
    If is_degraded is True, it means the device is missing
322
    redundancy. This is usually a sign that something went wrong in
323
    the device setup, if sync_percent is None.
324

325
    The ldisk parameter represents the degradation of the local
326
    data. This is only valid for some devices, the rest will always
327
    return False (not degraded).
328

329
    @rtype: objects.BlockDevStatus
330

331
    """
332
    return objects.BlockDevStatus(dev_path=self.dev_path,
333
                                  major=self.major,
334
                                  minor=self.minor,
335
                                  sync_percent=None,
336
                                  estimated_time=None,
337
                                  is_degraded=False,
338
                                  ldisk_status=constants.LDS_OKAY)
339

    
340
  def CombinedSyncStatus(self):
341
    """Calculate the mirror status recursively for our children.
342

343
    The return value is the same as for `GetSyncStatus()` except the
344
    minimum percent and maximum time are calculated across our
345
    children.
346

347
    @rtype: objects.BlockDevStatus
348

349
    """
350
    status = self.GetSyncStatus()
351

    
352
    min_percent = status.sync_percent
353
    max_time = status.estimated_time
354
    is_degraded = status.is_degraded
355
    ldisk_status = status.ldisk_status
356

    
357
    if self._children:
358
      for child in self._children:
359
        child_status = child.GetSyncStatus()
360

    
361
        if min_percent is None:
362
          min_percent = child_status.sync_percent
363
        elif child_status.sync_percent is not None:
364
          min_percent = min(min_percent, child_status.sync_percent)
365

    
366
        if max_time is None:
367
          max_time = child_status.estimated_time
368
        elif child_status.estimated_time is not None:
369
          max_time = max(max_time, child_status.estimated_time)
370

    
371
        is_degraded = is_degraded or child_status.is_degraded
372

    
373
        if ldisk_status is None:
374
          ldisk_status = child_status.ldisk_status
375
        elif child_status.ldisk_status is not None:
376
          ldisk_status = max(ldisk_status, child_status.ldisk_status)
377

    
378
    return objects.BlockDevStatus(dev_path=self.dev_path,
379
                                  major=self.major,
380
                                  minor=self.minor,
381
                                  sync_percent=min_percent,
382
                                  estimated_time=max_time,
383
                                  is_degraded=is_degraded,
384
                                  ldisk_status=ldisk_status)
385

    
386
  def SetInfo(self, text):
387
    """Update metadata with info text.
388

389
    Only supported for some device types.
390

391
    """
392
    for child in self._children:
393
      child.SetInfo(text)
394

    
395
  def Grow(self, amount, dryrun, backingstore):
396
    """Grow the block device.
397

398
    @type amount: integer
399
    @param amount: the amount (in mebibytes) to grow with
400
    @type dryrun: boolean
401
    @param dryrun: whether to execute the operation in simulation mode
402
        only, without actually increasing the size
403
    @param backingstore: whether to execute the operation on backing storage
404
        only, or on "logical" storage only; e.g. DRBD is logical storage,
405
        whereas LVM, file, RBD are backing storage
406

407
    """
408
    raise NotImplementedError
409

    
410
  def GetActualSize(self):
411
    """Return the actual disk size.
412

413
    @note: the device needs to be active when this is called
414

415
    """
416
    assert self.attached, "BlockDevice not attached in GetActualSize()"
417
    result = utils.RunCmd(["blockdev", "--getsize64", self.dev_path])
418
    if result.failed:
419
      _ThrowError("blockdev failed (%s): %s",
420
                  result.fail_reason, result.output)
421
    try:
422
      sz = int(result.output.strip())
423
    except (ValueError, TypeError), err:
424
      _ThrowError("Failed to parse blockdev output: %s", str(err))
425
    return sz
426

    
427
  def __repr__(self):
428
    return ("<%s: unique_id: %s, children: %s, %s:%s, %s>" %
429
            (self.__class__, self.unique_id, self._children,
430
             self.major, self.minor, self.dev_path))
431

    
432

    
433
class LogicalVolume(BlockDev):
434
  """Logical Volume block device.
435

436
  """
437
  _VALID_NAME_RE = re.compile("^[a-zA-Z0-9+_.-]*$")
438
  _INVALID_NAMES = frozenset([".", "..", "snapshot", "pvmove"])
439
  _INVALID_SUBSTRINGS = frozenset(["_mlog", "_mimage"])
440

    
441
  def __init__(self, unique_id, children, size, params):
442
    """Attaches to a LV device.
443

444
    The unique_id is a tuple (vg_name, lv_name)
445

446
    """
447
    super(LogicalVolume, self).__init__(unique_id, children, size, params)
448
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
449
      raise ValueError("Invalid configuration data %s" % str(unique_id))
450
    self._vg_name, self._lv_name = unique_id
451
    self._ValidateName(self._vg_name)
452
    self._ValidateName(self._lv_name)
453
    self.dev_path = utils.PathJoin("/dev", self._vg_name, self._lv_name)
454
    self._degraded = True
455
    self.major = self.minor = self.pe_size = self.stripe_count = None
456
    self.Attach()
457

    
458
  @classmethod
459
  def Create(cls, unique_id, children, size, params):
460
    """Create a new logical volume.
461

462
    """
463
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
464
      raise errors.ProgrammerError("Invalid configuration data %s" %
465
                                   str(unique_id))
466
    vg_name, lv_name = unique_id
467
    cls._ValidateName(vg_name)
468
    cls._ValidateName(lv_name)
469
    pvs_info = cls.GetPVInfo([vg_name])
470
    if not pvs_info:
471
      _ThrowError("Can't compute PV info for vg %s", vg_name)
472
    pvs_info.sort()
473
    pvs_info.reverse()
474

    
475
    pvlist = [pv[1] for pv in pvs_info]
476
    if compat.any(":" in v for v in pvlist):
477
      _ThrowError("Some of your PVs have the invalid character ':' in their"
478
                  " name, this is not supported - please filter them out"
479
                  " in lvm.conf using either 'filter' or 'preferred_names'")
480
    free_size = sum([pv[0] for pv in pvs_info])
481
    current_pvs = len(pvlist)
482
    desired_stripes = params[constants.LDP_STRIPES]
483
    stripes = min(current_pvs, desired_stripes)
484
    if stripes < desired_stripes:
485
      logging.warning("Could not use %d stripes for VG %s, as only %d PVs are"
486
                      " available.", desired_stripes, vg_name, current_pvs)
487

    
488
    # The size constraint should have been checked from the master before
489
    # calling the create function.
490
    if free_size < size:
491
      _ThrowError("Not enough free space: required %s,"
492
                  " available %s", size, free_size)
493
    cmd = ["lvcreate", "-L%dm" % size, "-n%s" % lv_name]
494
    # If the free space is not well distributed, we won't be able to
495
    # create an optimally-striped volume; in that case, we want to try
496
    # with N, N-1, ..., 2, and finally 1 (non-stripped) number of
497
    # stripes
498
    for stripes_arg in range(stripes, 0, -1):
499
      result = utils.RunCmd(cmd + ["-i%d" % stripes_arg] + [vg_name] + pvlist)
500
      if not result.failed:
501
        break
502
    if result.failed:
503
      _ThrowError("LV create failed (%s): %s",
504
                  result.fail_reason, result.output)
505
    return LogicalVolume(unique_id, children, size, params)
506

    
507
  @staticmethod
508
  def _GetVolumeInfo(lvm_cmd, fields):
509
    """Returns LVM Volumen infos using lvm_cmd
510

511
    @param lvm_cmd: Should be one of "pvs", "vgs" or "lvs"
512
    @param fields: Fields to return
513
    @return: A list of dicts each with the parsed fields
514

515
    """
516
    if not fields:
517
      raise errors.ProgrammerError("No fields specified")
518

    
519
    sep = "|"
520
    cmd = [lvm_cmd, "--noheadings", "--nosuffix", "--units=m", "--unbuffered",
521
           "--separator=%s" % sep, "-o%s" % ",".join(fields)]
522

    
523
    result = utils.RunCmd(cmd)
524
    if result.failed:
525
      raise errors.CommandError("Can't get the volume information: %s - %s" %
526
                                (result.fail_reason, result.output))
527

    
528
    data = []
529
    for line in result.stdout.splitlines():
530
      splitted_fields = line.strip().split(sep)
531

    
532
      if len(fields) != len(splitted_fields):
533
        raise errors.CommandError("Can't parse %s output: line '%s'" %
534
                                  (lvm_cmd, line))
535

    
536
      data.append(splitted_fields)
537

    
538
    return data
539

    
540
  @classmethod
541
  def GetPVInfo(cls, vg_names, filter_allocatable=True):
542
    """Get the free space info for PVs in a volume group.
543

544
    @param vg_names: list of volume group names, if empty all will be returned
545
    @param filter_allocatable: whether to skip over unallocatable PVs
546

547
    @rtype: list
548
    @return: list of tuples (free_space, name) with free_space in mebibytes
549

550
    """
551
    try:
552
      info = cls._GetVolumeInfo("pvs", ["pv_name", "vg_name", "pv_free",
553
                                        "pv_attr"])
554
    except errors.GenericError, err:
555
      logging.error("Can't get PV information: %s", err)
556
      return None
557

    
558
    data = []
559
    for pv_name, vg_name, pv_free, pv_attr in info:
560
      # (possibly) skip over pvs which are not allocatable
561
      if filter_allocatable and pv_attr[0] != "a":
562
        continue
563
      # (possibly) skip over pvs which are not in the right volume group(s)
564
      if vg_names and vg_name not in vg_names:
565
        continue
566
      data.append((float(pv_free), pv_name, vg_name))
567

    
568
    return data
569

    
570
  @classmethod
571
  def GetVGInfo(cls, vg_names, filter_readonly=True):
572
    """Get the free space info for specific VGs.
573

574
    @param vg_names: list of volume group names, if empty all will be returned
575
    @param filter_readonly: whether to skip over readonly VGs
576

577
    @rtype: list
578
    @return: list of tuples (free_space, total_size, name) with free_space in
579
             MiB
580

581
    """
582
    try:
583
      info = cls._GetVolumeInfo("vgs", ["vg_name", "vg_free", "vg_attr",
584
                                        "vg_size"])
585
    except errors.GenericError, err:
586
      logging.error("Can't get VG information: %s", err)
587
      return None
588

    
589
    data = []
590
    for vg_name, vg_free, vg_attr, vg_size in info:
591
      # (possibly) skip over vgs which are not writable
592
      if filter_readonly and vg_attr[0] == "r":
593
        continue
594
      # (possibly) skip over vgs which are not in the right volume group(s)
595
      if vg_names and vg_name not in vg_names:
596
        continue
597
      data.append((float(vg_free), float(vg_size), vg_name))
598

    
599
    return data
600

    
601
  @classmethod
602
  def _ValidateName(cls, name):
603
    """Validates that a given name is valid as VG or LV name.
604

605
    The list of valid characters and restricted names is taken out of
606
    the lvm(8) manpage, with the simplification that we enforce both
607
    VG and LV restrictions on the names.
608

609
    """
610
    if (not cls._VALID_NAME_RE.match(name) or
611
        name in cls._INVALID_NAMES or
612
        compat.any(substring in name for substring in cls._INVALID_SUBSTRINGS)):
613
      _ThrowError("Invalid LVM name '%s'", name)
614

    
615
  def Remove(self):
616
    """Remove this logical volume.
617

618
    """
619
    if not self.minor and not self.Attach():
620
      # the LV does not exist
621
      return
622
    result = utils.RunCmd(["lvremove", "-f", "%s/%s" %
623
                           (self._vg_name, self._lv_name)])
624
    if result.failed:
625
      _ThrowError("Can't lvremove: %s - %s", result.fail_reason, result.output)
626

    
627
  def Rename(self, new_id):
628
    """Rename this logical volume.
629

630
    """
631
    if not isinstance(new_id, (tuple, list)) or len(new_id) != 2:
632
      raise errors.ProgrammerError("Invalid new logical id '%s'" % new_id)
633
    new_vg, new_name = new_id
634
    if new_vg != self._vg_name:
635
      raise errors.ProgrammerError("Can't move a logical volume across"
636
                                   " volume groups (from %s to to %s)" %
637
                                   (self._vg_name, new_vg))
638
    result = utils.RunCmd(["lvrename", new_vg, self._lv_name, new_name])
639
    if result.failed:
640
      _ThrowError("Failed to rename the logical volume: %s", result.output)
641
    self._lv_name = new_name
642
    self.dev_path = utils.PathJoin("/dev", self._vg_name, self._lv_name)
643

    
644
  def Attach(self):
645
    """Attach to an existing LV.
646

647
    This method will try to see if an existing and active LV exists
648
    which matches our name. If so, its major/minor will be
649
    recorded.
650

651
    """
652
    self.attached = False
653
    result = utils.RunCmd(["lvs", "--noheadings", "--separator=,",
654
                           "--units=m", "--nosuffix",
655
                           "-olv_attr,lv_kernel_major,lv_kernel_minor,"
656
                           "vg_extent_size,stripes", self.dev_path])
657
    if result.failed:
658
      logging.error("Can't find LV %s: %s, %s",
659
                    self.dev_path, result.fail_reason, result.output)
660
      return False
661
    # the output can (and will) have multiple lines for multi-segment
662
    # LVs, as the 'stripes' parameter is a segment one, so we take
663
    # only the last entry, which is the one we're interested in; note
664
    # that with LVM2 anyway the 'stripes' value must be constant
665
    # across segments, so this is a no-op actually
666
    out = result.stdout.splitlines()
667
    if not out: # totally empty result? splitlines() returns at least
668
                # one line for any non-empty string
669
      logging.error("Can't parse LVS output, no lines? Got '%s'", str(out))
670
      return False
671
    out = out[-1].strip().rstrip(",")
672
    out = out.split(",")
673
    if len(out) != 5:
674
      logging.error("Can't parse LVS output, len(%s) != 5", str(out))
675
      return False
676

    
677
    status, major, minor, pe_size, stripes = out
678
    if len(status) < 6:
679
      logging.error("lvs lv_attr is not at least 6 characters (%s)", status)
680
      return False
681

    
682
    try:
683
      major = int(major)
684
      minor = int(minor)
685
    except (TypeError, ValueError), err:
686
      logging.error("lvs major/minor cannot be parsed: %s", str(err))
687

    
688
    try:
689
      pe_size = int(float(pe_size))
690
    except (TypeError, ValueError), err:
691
      logging.error("Can't parse vg extent size: %s", err)
692
      return False
693

    
694
    try:
695
      stripes = int(stripes)
696
    except (TypeError, ValueError), err:
697
      logging.error("Can't parse the number of stripes: %s", err)
698
      return False
699

    
700
    self.major = major
701
    self.minor = minor
702
    self.pe_size = pe_size
703
    self.stripe_count = stripes
704
    self._degraded = status[0] == "v" # virtual volume, i.e. doesn't backing
705
                                      # storage
706
    self.attached = True
707
    return True
708

    
709
  def Assemble(self):
710
    """Assemble the device.
711

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

716
    """
717
    result = utils.RunCmd(["lvchange", "-ay", self.dev_path])
718
    if result.failed:
719
      _ThrowError("Can't activate lv %s: %s", self.dev_path, result.output)
720

    
721
  def Shutdown(self):
722
    """Shutdown the device.
723

724
    This is a no-op for the LV device type, as we don't deactivate the
725
    volumes on shutdown.
726

727
    """
728
    pass
729

    
730
  def GetSyncStatus(self):
731
    """Returns the sync status of the device.
732

733
    If this device is a mirroring device, this function returns the
734
    status of the mirror.
735

736
    For logical volumes, sync_percent and estimated_time are always
737
    None (no recovery in progress, as we don't handle the mirrored LV
738
    case). The is_degraded parameter is the inverse of the ldisk
739
    parameter.
740

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

747
    The status was already read in Attach, so we just return it.
748

749
    @rtype: objects.BlockDevStatus
750

751
    """
752
    if self._degraded:
753
      ldisk_status = constants.LDS_FAULTY
754
    else:
755
      ldisk_status = constants.LDS_OKAY
756

    
757
    return objects.BlockDevStatus(dev_path=self.dev_path,
758
                                  major=self.major,
759
                                  minor=self.minor,
760
                                  sync_percent=None,
761
                                  estimated_time=None,
762
                                  is_degraded=self._degraded,
763
                                  ldisk_status=ldisk_status)
764

    
765
  def Open(self, force=False):
766
    """Make the device ready for I/O.
767

768
    This is a no-op for the LV device type.
769

770
    """
771
    pass
772

    
773
  def Close(self):
774
    """Notifies that the device will no longer be used for I/O.
775

776
    This is a no-op for the LV device type.
777

778
    """
779
    pass
780

    
781
  def Snapshot(self, size):
782
    """Create a snapshot copy of an lvm block device.
783

784
    @returns: tuple (vg, lv)
785

786
    """
787
    snap_name = self._lv_name + ".snap"
788

    
789
    # remove existing snapshot if found
790
    snap = LogicalVolume((self._vg_name, snap_name), None, size, self.params)
791
    _IgnoreError(snap.Remove)
792

    
793
    vg_info = self.GetVGInfo([self._vg_name])
794
    if not vg_info:
795
      _ThrowError("Can't compute VG info for vg %s", self._vg_name)
796
    free_size, _, _ = vg_info[0]
797
    if free_size < size:
798
      _ThrowError("Not enough free space: required %s,"
799
                  " available %s", size, free_size)
800

    
801
    result = utils.RunCmd(["lvcreate", "-L%dm" % size, "-s",
802
                           "-n%s" % snap_name, self.dev_path])
803
    if result.failed:
804
      _ThrowError("command: %s error: %s - %s",
805
                  result.cmd, result.fail_reason, result.output)
806

    
807
    return (self._vg_name, snap_name)
808

    
809
  def SetInfo(self, text):
810
    """Update metadata with info text.
811

812
    """
813
    BlockDev.SetInfo(self, text)
814

    
815
    # Replace invalid characters
816
    text = re.sub("^[^A-Za-z0-9_+.]", "_", text)
817
    text = re.sub("[^-A-Za-z0-9_+.]", "_", text)
818

    
819
    # Only up to 128 characters are allowed
820
    text = text[:128]
821

    
822
    result = utils.RunCmd(["lvchange", "--addtag", text,
823
                           self.dev_path])
824
    if result.failed:
825
      _ThrowError("Command: %s error: %s - %s", result.cmd, result.fail_reason,
826
                  result.output)
827

    
828
  def Grow(self, amount, dryrun, backingstore):
829
    """Grow the logical volume.
830

831
    """
832
    if not backingstore:
833
      return
834
    if self.pe_size is None or self.stripe_count is None:
835
      if not self.Attach():
836
        _ThrowError("Can't attach to LV during Grow()")
837
    full_stripe_size = self.pe_size * self.stripe_count
838
    rest = amount % full_stripe_size
839
    if rest != 0:
840
      amount += full_stripe_size - rest
841
    cmd = ["lvextend", "-L", "+%dm" % amount]
842
    if dryrun:
843
      cmd.append("--test")
844
    # we try multiple algorithms since the 'best' ones might not have
845
    # space available in the right place, but later ones might (since
846
    # they have less constraints); also note that only recent LVM
847
    # supports 'cling'
848
    for alloc_policy in "contiguous", "cling", "normal":
849
      result = utils.RunCmd(cmd + ["--alloc", alloc_policy, self.dev_path])
850
      if not result.failed:
851
        return
852
    _ThrowError("Can't grow LV %s: %s", self.dev_path, result.output)
853

    
854

    
855
class DRBD8Status(object):
856
  """A DRBD status representation class.
857

858
  Note that this doesn't support unconfigured devices (cs:Unconfigured).
859

860
  """
861
  UNCONF_RE = re.compile(r"\s*[0-9]+:\s*cs:Unconfigured$")
862
  LINE_RE = re.compile(r"\s*[0-9]+:\s*cs:(\S+)\s+(?:st|ro):([^/]+)/(\S+)"
863
                       "\s+ds:([^/]+)/(\S+)\s+.*$")
864
  SYNC_RE = re.compile(r"^.*\ssync'ed:\s*([0-9.]+)%.*"
865
                       # Due to a bug in drbd in the kernel, introduced in
866
                       # commit 4b0715f096 (still unfixed as of 2011-08-22)
867
                       "(?:\s|M)"
868
                       "finish: ([0-9]+):([0-9]+):([0-9]+)\s.*$")
869

    
870
  CS_UNCONFIGURED = "Unconfigured"
871
  CS_STANDALONE = "StandAlone"
872
  CS_WFCONNECTION = "WFConnection"
873
  CS_WFREPORTPARAMS = "WFReportParams"
874
  CS_CONNECTED = "Connected"
875
  CS_STARTINGSYNCS = "StartingSyncS"
876
  CS_STARTINGSYNCT = "StartingSyncT"
877
  CS_WFBITMAPS = "WFBitMapS"
878
  CS_WFBITMAPT = "WFBitMapT"
879
  CS_WFSYNCUUID = "WFSyncUUID"
880
  CS_SYNCSOURCE = "SyncSource"
881
  CS_SYNCTARGET = "SyncTarget"
882
  CS_PAUSEDSYNCS = "PausedSyncS"
883
  CS_PAUSEDSYNCT = "PausedSyncT"
884
  CSET_SYNC = frozenset([
885
    CS_WFREPORTPARAMS,
886
    CS_STARTINGSYNCS,
887
    CS_STARTINGSYNCT,
888
    CS_WFBITMAPS,
889
    CS_WFBITMAPT,
890
    CS_WFSYNCUUID,
891
    CS_SYNCSOURCE,
892
    CS_SYNCTARGET,
893
    CS_PAUSEDSYNCS,
894
    CS_PAUSEDSYNCT,
895
    ])
896

    
897
  DS_DISKLESS = "Diskless"
898
  DS_ATTACHING = "Attaching" # transient state
899
  DS_FAILED = "Failed" # transient state, next: diskless
900
  DS_NEGOTIATING = "Negotiating" # transient state
901
  DS_INCONSISTENT = "Inconsistent" # while syncing or after creation
902
  DS_OUTDATED = "Outdated"
903
  DS_DUNKNOWN = "DUnknown" # shown for peer disk when not connected
904
  DS_CONSISTENT = "Consistent"
905
  DS_UPTODATE = "UpToDate" # normal state
906

    
907
  RO_PRIMARY = "Primary"
908
  RO_SECONDARY = "Secondary"
909
  RO_UNKNOWN = "Unknown"
910

    
911
  def __init__(self, procline):
912
    u = self.UNCONF_RE.match(procline)
913
    if u:
914
      self.cstatus = self.CS_UNCONFIGURED
915
      self.lrole = self.rrole = self.ldisk = self.rdisk = None
916
    else:
917
      m = self.LINE_RE.match(procline)
918
      if not m:
919
        raise errors.BlockDeviceError("Can't parse input data '%s'" % procline)
920
      self.cstatus = m.group(1)
921
      self.lrole = m.group(2)
922
      self.rrole = m.group(3)
923
      self.ldisk = m.group(4)
924
      self.rdisk = m.group(5)
925

    
926
    # end reading of data from the LINE_RE or UNCONF_RE
927

    
928
    self.is_standalone = self.cstatus == self.CS_STANDALONE
929
    self.is_wfconn = self.cstatus == self.CS_WFCONNECTION
930
    self.is_connected = self.cstatus == self.CS_CONNECTED
931
    self.is_primary = self.lrole == self.RO_PRIMARY
932
    self.is_secondary = self.lrole == self.RO_SECONDARY
933
    self.peer_primary = self.rrole == self.RO_PRIMARY
934
    self.peer_secondary = self.rrole == self.RO_SECONDARY
935
    self.both_primary = self.is_primary and self.peer_primary
936
    self.both_secondary = self.is_secondary and self.peer_secondary
937

    
938
    self.is_diskless = self.ldisk == self.DS_DISKLESS
939
    self.is_disk_uptodate = self.ldisk == self.DS_UPTODATE
940

    
941
    self.is_in_resync = self.cstatus in self.CSET_SYNC
942
    self.is_in_use = self.cstatus != self.CS_UNCONFIGURED
943

    
944
    m = self.SYNC_RE.match(procline)
945
    if m:
946
      self.sync_percent = float(m.group(1))
947
      hours = int(m.group(2))
948
      minutes = int(m.group(3))
949
      seconds = int(m.group(4))
950
      self.est_time = hours * 3600 + minutes * 60 + seconds
951
    else:
952
      # we have (in this if branch) no percent information, but if
953
      # we're resyncing we need to 'fake' a sync percent information,
954
      # as this is how cmdlib determines if it makes sense to wait for
955
      # resyncing or not
956
      if self.is_in_resync:
957
        self.sync_percent = 0
958
      else:
959
        self.sync_percent = None
960
      self.est_time = None
961

    
962

    
963
class BaseDRBD(BlockDev): # pylint: disable=W0223
964
  """Base DRBD class.
965

966
  This class contains a few bits of common functionality between the
967
  0.7 and 8.x versions of DRBD.
968

969
  """
970
  _VERSION_RE = re.compile(r"^version: (\d+)\.(\d+)\.(\d+)(?:\.\d+)?"
971
                           r" \(api:(\d+)/proto:(\d+)(?:-(\d+))?\)")
972
  _VALID_LINE_RE = re.compile("^ *([0-9]+): cs:([^ ]+).*$")
973
  _UNUSED_LINE_RE = re.compile("^ *([0-9]+): cs:Unconfigured$")
974

    
975
  _DRBD_MAJOR = 147
976
  _ST_UNCONFIGURED = "Unconfigured"
977
  _ST_WFCONNECTION = "WFConnection"
978
  _ST_CONNECTED = "Connected"
979

    
980
  _STATUS_FILE = "/proc/drbd"
981
  _USERMODE_HELPER_FILE = "/sys/module/drbd/parameters/usermode_helper"
982

    
983
  @staticmethod
984
  def _GetProcData(filename=_STATUS_FILE):
985
    """Return data from /proc/drbd.
986

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

    
1000
  @classmethod
1001
  def _MassageProcData(cls, data):
1002
    """Transform the output of _GetProdData into a nicer form.
1003

1004
    @return: a dictionary of minor: joined lines from /proc/drbd
1005
        for that minor
1006

1007
    """
1008
    results = {}
1009
    old_minor = old_line = None
1010
    for line in data:
1011
      if not line: # completely empty lines, as can be returned by drbd8.0+
1012
        continue
1013
      lresult = cls._VALID_LINE_RE.match(line)
1014
      if lresult is not None:
1015
        if old_minor is not None:
1016
          results[old_minor] = old_line
1017
        old_minor = int(lresult.group(1))
1018
        old_line = line
1019
      else:
1020
        if old_minor is not None:
1021
          old_line += " " + line.strip()
1022
    # add last line
1023
    if old_minor is not None:
1024
      results[old_minor] = old_line
1025
    return results
1026

    
1027
  @classmethod
1028
  def _GetVersion(cls, proc_data):
1029
    """Return the DRBD version.
1030

1031
    This will return a dict with keys:
1032
      - k_major
1033
      - k_minor
1034
      - k_point
1035
      - api
1036
      - proto
1037
      - proto2 (only on drbd > 8.2.X)
1038

1039
    """
1040
    first_line = proc_data[0].strip()
1041
    version = cls._VERSION_RE.match(first_line)
1042
    if not version:
1043
      raise errors.BlockDeviceError("Can't parse DRBD version from '%s'" %
1044
                                    first_line)
1045

    
1046
    values = version.groups()
1047
    retval = {
1048
      "k_major": int(values[0]),
1049
      "k_minor": int(values[1]),
1050
      "k_point": int(values[2]),
1051
      "api": int(values[3]),
1052
      "proto": int(values[4]),
1053
      }
1054
    if values[5] is not None:
1055
      retval["proto2"] = values[5]
1056

    
1057
    return retval
1058

    
1059
  @staticmethod
1060
  def GetUsermodeHelper(filename=_USERMODE_HELPER_FILE):
1061
    """Returns DRBD usermode_helper currently set.
1062

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

    
1076
  @staticmethod
1077
  def _DevPath(minor):
1078
    """Return the path to a drbd device for a given minor.
1079

1080
    """
1081
    return "/dev/drbd%d" % minor
1082

    
1083
  @classmethod
1084
  def GetUsedDevs(cls):
1085
    """Compute the list of used DRBD devices.
1086

1087
    """
1088
    data = cls._GetProcData()
1089

    
1090
    used_devs = {}
1091
    for line in data:
1092
      match = cls._VALID_LINE_RE.match(line)
1093
      if not match:
1094
        continue
1095
      minor = int(match.group(1))
1096
      state = match.group(2)
1097
      if state == cls._ST_UNCONFIGURED:
1098
        continue
1099
      used_devs[minor] = state, line
1100

    
1101
    return used_devs
1102

    
1103
  def _SetFromMinor(self, minor):
1104
    """Set our parameters based on the given minor.
1105

1106
    This sets our minor variable and our dev_path.
1107

1108
    """
1109
    if minor is None:
1110
      self.minor = self.dev_path = None
1111
      self.attached = False
1112
    else:
1113
      self.minor = minor
1114
      self.dev_path = self._DevPath(minor)
1115
      self.attached = True
1116

    
1117
  @staticmethod
1118
  def _CheckMetaSize(meta_device):
1119
    """Check if the given meta device looks like a valid one.
1120

1121
    This currently only checks the size, which must be around
1122
    128MiB.
1123

1124
    """
1125
    result = utils.RunCmd(["blockdev", "--getsize", meta_device])
1126
    if result.failed:
1127
      _ThrowError("Failed to get device size: %s - %s",
1128
                  result.fail_reason, result.output)
1129
    try:
1130
      sectors = int(result.stdout)
1131
    except (TypeError, ValueError):
1132
      _ThrowError("Invalid output from blockdev: '%s'", result.stdout)
1133
    num_bytes = sectors * 512
1134
    if num_bytes < 128 * 1024 * 1024: # less than 128MiB
1135
      _ThrowError("Meta device too small (%.2fMib)", (num_bytes / 1024 / 1024))
1136
    # the maximum *valid* size of the meta device when living on top
1137
    # of LVM is hard to compute: it depends on the number of stripes
1138
    # and the PE size; e.g. a 2-stripe, 64MB PE will result in a 128MB
1139
    # (normal size), but an eight-stripe 128MB PE will result in a 1GB
1140
    # size meta device; as such, we restrict it to 1GB (a little bit
1141
    # too generous, but making assumptions about PE size is hard)
1142
    if num_bytes > 1024 * 1024 * 1024:
1143
      _ThrowError("Meta device too big (%.2fMiB)", (num_bytes / 1024 / 1024))
1144

    
1145
  def Rename(self, new_id):
1146
    """Rename a device.
1147

1148
    This is not supported for drbd devices.
1149

1150
    """
1151
    raise errors.ProgrammerError("Can't rename a drbd device")
1152

    
1153

    
1154
class DRBD8(BaseDRBD):
1155
  """DRBD v8.x block device.
1156

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

1161
  The unique_id for the drbd device is a (local_ip, local_port,
1162
  remote_ip, remote_port, local_minor, secret) tuple, and it must have
1163
  two children: the data device and the meta_device. The meta device
1164
  is checked for valid size and is zeroed on create.
1165

1166
  """
1167
  _MAX_MINORS = 255
1168
  _PARSE_SHOW = None
1169

    
1170
  # timeout constants
1171
  _NET_RECONFIG_TIMEOUT = 60
1172

    
1173
  # command line options for barriers
1174
  _DISABLE_DISK_OPTION = "--no-disk-barrier"  # -a
1175
  _DISABLE_DRAIN_OPTION = "--no-disk-drain"   # -D
1176
  _DISABLE_FLUSH_OPTION = "--no-disk-flushes" # -i
1177
  _DISABLE_META_FLUSH_OPTION = "--no-md-flushes"  # -m
1178

    
1179
  def __init__(self, unique_id, children, size, params):
1180
    if children and children.count(None) > 0:
1181
      children = []
1182
    if len(children) not in (0, 2):
1183
      raise ValueError("Invalid configuration data %s" % str(children))
1184
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 6:
1185
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1186
    (self._lhost, self._lport,
1187
     self._rhost, self._rport,
1188
     self._aminor, self._secret) = unique_id
1189
    if children:
1190
      if not _CanReadDevice(children[1].dev_path):
1191
        logging.info("drbd%s: Ignoring unreadable meta device", self._aminor)
1192
        children = []
1193
    super(DRBD8, self).__init__(unique_id, children, size, params)
1194
    self.major = self._DRBD_MAJOR
1195
    version = self._GetVersion(self._GetProcData())
1196
    if version["k_major"] != 8:
1197
      _ThrowError("Mismatch in DRBD kernel version and requested ganeti"
1198
                  " usage: kernel is %s.%s, ganeti wants 8.x",
1199
                  version["k_major"], version["k_minor"])
1200

    
1201
    if (self._lhost is not None and self._lhost == self._rhost and
1202
        self._lport == self._rport):
1203
      raise ValueError("Invalid configuration data, same local/remote %s" %
1204
                       (unique_id,))
1205
    self.Attach()
1206

    
1207
  @classmethod
1208
  def _InitMeta(cls, minor, dev_path):
1209
    """Initialize a meta device.
1210

1211
    This will not work if the given minor is in use.
1212

1213
    """
1214
    # Zero the metadata first, in order to make sure drbdmeta doesn't
1215
    # try to auto-detect existing filesystems or similar (see
1216
    # http://code.google.com/p/ganeti/issues/detail?id=182); we only
1217
    # care about the first 128MB of data in the device, even though it
1218
    # can be bigger
1219
    result = utils.RunCmd([constants.DD_CMD,
1220
                           "if=/dev/zero", "of=%s" % dev_path,
1221
                           "bs=1048576", "count=128", "oflag=direct"])
1222
    if result.failed:
1223
      _ThrowError("Can't wipe the meta device: %s", result.output)
1224

    
1225
    result = utils.RunCmd(["drbdmeta", "--force", cls._DevPath(minor),
1226
                           "v08", dev_path, "0", "create-md"])
1227
    if result.failed:
1228
      _ThrowError("Can't initialize meta device: %s", result.output)
1229

    
1230
  @classmethod
1231
  def _FindUnusedMinor(cls):
1232
    """Find an unused DRBD device.
1233

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

1237
    """
1238
    data = cls._GetProcData()
1239

    
1240
    highest = None
1241
    for line in data:
1242
      match = cls._UNUSED_LINE_RE.match(line)
1243
      if match:
1244
        return int(match.group(1))
1245
      match = cls._VALID_LINE_RE.match(line)
1246
      if match:
1247
        minor = int(match.group(1))
1248
        highest = max(highest, minor)
1249
    if highest is None: # there are no minors in use at all
1250
      return 0
1251
    if highest >= cls._MAX_MINORS:
1252
      logging.error("Error: no free drbd minors!")
1253
      raise errors.BlockDeviceError("Can't find a free DRBD minor")
1254
    return highest + 1
1255

    
1256
  @classmethod
1257
  def _GetShowParser(cls):
1258
    """Return a parser for `drbd show` output.
1259

1260
    This will either create or return an already-created parser for the
1261
    output of the command `drbd show`.
1262

1263
    """
1264
    if cls._PARSE_SHOW is not None:
1265
      return cls._PARSE_SHOW
1266

    
1267
    # pyparsing setup
1268
    lbrace = pyp.Literal("{").suppress()
1269
    rbrace = pyp.Literal("}").suppress()
1270
    lbracket = pyp.Literal("[").suppress()
1271
    rbracket = pyp.Literal("]").suppress()
1272
    semi = pyp.Literal(";").suppress()
1273
    colon = pyp.Literal(":").suppress()
1274
    # this also converts the value to an int
1275
    number = pyp.Word(pyp.nums).setParseAction(lambda s, l, t: int(t[0]))
1276

    
1277
    comment = pyp.Literal("#") + pyp.Optional(pyp.restOfLine)
1278
    defa = pyp.Literal("_is_default").suppress()
1279
    dbl_quote = pyp.Literal('"').suppress()
1280

    
1281
    keyword = pyp.Word(pyp.alphanums + "-")
1282

    
1283
    # value types
1284
    value = pyp.Word(pyp.alphanums + "_-/.:")
1285
    quoted = dbl_quote + pyp.CharsNotIn('"') + dbl_quote
1286
    ipv4_addr = (pyp.Optional(pyp.Literal("ipv4")).suppress() +
1287
                 pyp.Word(pyp.nums + ".") + colon + number)
1288
    ipv6_addr = (pyp.Optional(pyp.Literal("ipv6")).suppress() +
1289
                 pyp.Optional(lbracket) + pyp.Word(pyp.hexnums + ":") +
1290
                 pyp.Optional(rbracket) + colon + number)
1291
    # meta device, extended syntax
1292
    meta_value = ((value ^ quoted) + lbracket + number + rbracket)
1293
    # device name, extended syntax
1294
    device_value = pyp.Literal("minor").suppress() + number
1295

    
1296
    # a statement
1297
    stmt = (~rbrace + keyword + ~lbrace +
1298
            pyp.Optional(ipv4_addr ^ ipv6_addr ^ value ^ quoted ^ meta_value ^
1299
                         device_value) +
1300
            pyp.Optional(defa) + semi +
1301
            pyp.Optional(pyp.restOfLine).suppress())
1302

    
1303
    # an entire section
1304
    section_name = pyp.Word(pyp.alphas + "_")
1305
    section = section_name + lbrace + pyp.ZeroOrMore(pyp.Group(stmt)) + rbrace
1306

    
1307
    bnf = pyp.ZeroOrMore(pyp.Group(section ^ stmt))
1308
    bnf.ignore(comment)
1309

    
1310
    cls._PARSE_SHOW = bnf
1311

    
1312
    return bnf
1313

    
1314
  @classmethod
1315
  def _GetShowData(cls, minor):
1316
    """Return the `drbdsetup show` data for a minor.
1317

1318
    """
1319
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "show"])
1320
    if result.failed:
1321
      logging.error("Can't display the drbd config: %s - %s",
1322
                    result.fail_reason, result.output)
1323
      return None
1324
    return result.stdout
1325

    
1326
  @classmethod
1327
  def _GetDevInfo(cls, out):
1328
    """Parse details about a given DRBD minor.
1329

1330
    This return, if available, the local backing device (as a path)
1331
    and the local and remote (ip, port) information from a string
1332
    containing the output of the `drbdsetup show` command as returned
1333
    by _GetShowData.
1334

1335
    """
1336
    data = {}
1337
    if not out:
1338
      return data
1339

    
1340
    bnf = cls._GetShowParser()
1341
    # run pyparse
1342

    
1343
    try:
1344
      results = bnf.parseString(out)
1345
    except pyp.ParseException, err:
1346
      _ThrowError("Can't parse drbdsetup show output: %s", str(err))
1347

    
1348
    # and massage the results into our desired format
1349
    for section in results:
1350
      sname = section[0]
1351
      if sname == "_this_host":
1352
        for lst in section[1:]:
1353
          if lst[0] == "disk":
1354
            data["local_dev"] = lst[1]
1355
          elif lst[0] == "meta-disk":
1356
            data["meta_dev"] = lst[1]
1357
            data["meta_index"] = lst[2]
1358
          elif lst[0] == "address":
1359
            data["local_addr"] = tuple(lst[1:])
1360
      elif sname == "_remote_host":
1361
        for lst in section[1:]:
1362
          if lst[0] == "address":
1363
            data["remote_addr"] = tuple(lst[1:])
1364
    return data
1365

    
1366
  def _MatchesLocal(self, info):
1367
    """Test if our local config matches with an existing device.
1368

1369
    The parameter should be as returned from `_GetDevInfo()`. This
1370
    method tests if our local backing device is the same as the one in
1371
    the info parameter, in effect testing if we look like the given
1372
    device.
1373

1374
    """
1375
    if self._children:
1376
      backend, meta = self._children
1377
    else:
1378
      backend = meta = None
1379

    
1380
    if backend is not None:
1381
      retval = ("local_dev" in info and info["local_dev"] == backend.dev_path)
1382
    else:
1383
      retval = ("local_dev" not in info)
1384

    
1385
    if meta is not None:
1386
      retval = retval and ("meta_dev" in info and
1387
                           info["meta_dev"] == meta.dev_path)
1388
      retval = retval and ("meta_index" in info and
1389
                           info["meta_index"] == 0)
1390
    else:
1391
      retval = retval and ("meta_dev" not in info and
1392
                           "meta_index" not in info)
1393
    return retval
1394

    
1395
  def _MatchesNet(self, info):
1396
    """Test if our network config matches with an existing device.
1397

1398
    The parameter should be as returned from `_GetDevInfo()`. This
1399
    method tests if our network configuration is the same as the one
1400
    in the info parameter, in effect testing if we look like the given
1401
    device.
1402

1403
    """
1404
    if (((self._lhost is None and not ("local_addr" in info)) and
1405
         (self._rhost is None and not ("remote_addr" in info)))):
1406
      return True
1407

    
1408
    if self._lhost is None:
1409
      return False
1410

    
1411
    if not ("local_addr" in info and
1412
            "remote_addr" in info):
1413
      return False
1414

    
1415
    retval = (info["local_addr"] == (self._lhost, self._lport))
1416
    retval = (retval and
1417
              info["remote_addr"] == (self._rhost, self._rport))
1418
    return retval
1419

    
1420
  def _AssembleLocal(self, minor, backend, meta, size):
1421
    """Configure the local part of a DRBD device.
1422

1423
    """
1424
    args = ["drbdsetup", self._DevPath(minor), "disk",
1425
            backend, meta, "0",
1426
            "-e", "detach",
1427
            "--create-device"]
1428
    if size:
1429
      args.extend(["-d", "%sm" % size])
1430

    
1431
    version = self._GetVersion(self._GetProcData())
1432
    vmaj = version["k_major"]
1433
    vmin = version["k_minor"]
1434
    vrel = version["k_point"]
1435

    
1436
    barrier_args = \
1437
      self._ComputeDiskBarrierArgs(vmaj, vmin, vrel,
1438
                                   self.params[constants.LDP_BARRIERS],
1439
                                   self.params[constants.LDP_NO_META_FLUSH])
1440
    args.extend(barrier_args)
1441

    
1442
    if self.params[constants.LDP_DISK_CUSTOM]:
1443
      args.extend(shlex.split(self.params[constants.LDP_DISK_CUSTOM]))
1444

    
1445
    result = utils.RunCmd(args)
1446
    if result.failed:
1447
      _ThrowError("drbd%d: can't attach local disk: %s", minor, result.output)
1448

    
1449
  @classmethod
1450
  def _ComputeDiskBarrierArgs(cls, vmaj, vmin, vrel, disabled_barriers,
1451
                              disable_meta_flush):
1452
    """Compute the DRBD command line parameters for disk barriers
1453

1454
    Returns a list of the disk barrier parameters as requested via the
1455
    disabled_barriers and disable_meta_flush arguments, and according to the
1456
    supported ones in the DRBD version vmaj.vmin.vrel
1457

1458
    If the desired option is unsupported, raises errors.BlockDeviceError.
1459

1460
    """
1461
    disabled_barriers_set = frozenset(disabled_barriers)
1462
    if not disabled_barriers_set in constants.DRBD_VALID_BARRIER_OPT:
1463
      raise errors.BlockDeviceError("%s is not a valid option set for DRBD"
1464
                                    " barriers" % disabled_barriers)
1465

    
1466
    args = []
1467

    
1468
    # The following code assumes DRBD 8.x, with x < 4 and x != 1 (DRBD 8.1.x
1469
    # does not exist)
1470
    if not vmaj == 8 and vmin in (0, 2, 3):
1471
      raise errors.BlockDeviceError("Unsupported DRBD version: %d.%d.%d" %
1472
                                    (vmaj, vmin, vrel))
1473

    
1474
    def _AppendOrRaise(option, min_version):
1475
      """Helper for DRBD options"""
1476
      if min_version is not None and vrel >= min_version:
1477
        args.append(option)
1478
      else:
1479
        raise errors.BlockDeviceError("Could not use the option %s as the"
1480
                                      " DRBD version %d.%d.%d does not support"
1481
                                      " it." % (option, vmaj, vmin, vrel))
1482

    
1483
    # the minimum version for each feature is encoded via pairs of (minor
1484
    # version -> x) where x is version in which support for the option was
1485
    # introduced.
1486
    meta_flush_supported = disk_flush_supported = {
1487
      0: 12,
1488
      2: 7,
1489
      3: 0,
1490
      }
1491

    
1492
    disk_drain_supported = {
1493
      2: 7,
1494
      3: 0,
1495
      }
1496

    
1497
    disk_barriers_supported = {
1498
      3: 0,
1499
      }
1500

    
1501
    # meta flushes
1502
    if disable_meta_flush:
1503
      _AppendOrRaise(cls._DISABLE_META_FLUSH_OPTION,
1504
                     meta_flush_supported.get(vmin, None))
1505

    
1506
    # disk flushes
1507
    if constants.DRBD_B_DISK_FLUSH in disabled_barriers_set:
1508
      _AppendOrRaise(cls._DISABLE_FLUSH_OPTION,
1509
                     disk_flush_supported.get(vmin, None))
1510

    
1511
    # disk drain
1512
    if constants.DRBD_B_DISK_DRAIN in disabled_barriers_set:
1513
      _AppendOrRaise(cls._DISABLE_DRAIN_OPTION,
1514
                     disk_drain_supported.get(vmin, None))
1515

    
1516
    # disk barriers
1517
    if constants.DRBD_B_DISK_BARRIERS in disabled_barriers_set:
1518
      _AppendOrRaise(cls._DISABLE_DISK_OPTION,
1519
                     disk_barriers_supported.get(vmin, None))
1520

    
1521
    return args
1522

    
1523
  def _AssembleNet(self, minor, net_info, protocol,
1524
                   dual_pri=False, hmac=None, secret=None):
1525
    """Configure the network part of the device.
1526

1527
    """
1528
    lhost, lport, rhost, rport = net_info
1529
    if None in net_info:
1530
      # we don't want network connection and actually want to make
1531
      # sure its shutdown
1532
      self._ShutdownNet(minor)
1533
      return
1534

    
1535
    # Workaround for a race condition. When DRBD is doing its dance to
1536
    # establish a connection with its peer, it also sends the
1537
    # synchronization speed over the wire. In some cases setting the
1538
    # sync speed only after setting up both sides can race with DRBD
1539
    # connecting, hence we set it here before telling DRBD anything
1540
    # about its peer.
1541
    sync_errors = self._SetMinorSyncParams(minor, self.params)
1542
    if sync_errors:
1543
      _ThrowError("drbd%d: can't set the synchronization parameters: %s" %
1544
                  (minor, utils.CommaJoin(sync_errors)))
1545

    
1546
    if netutils.IP6Address.IsValid(lhost):
1547
      if not netutils.IP6Address.IsValid(rhost):
1548
        _ThrowError("drbd%d: can't connect ip %s to ip %s" %
1549
                    (minor, lhost, rhost))
1550
      family = "ipv6"
1551
    elif netutils.IP4Address.IsValid(lhost):
1552
      if not netutils.IP4Address.IsValid(rhost):
1553
        _ThrowError("drbd%d: can't connect ip %s to ip %s" %
1554
                    (minor, lhost, rhost))
1555
      family = "ipv4"
1556
    else:
1557
      _ThrowError("drbd%d: Invalid ip %s" % (minor, lhost))
1558

    
1559
    args = ["drbdsetup", self._DevPath(minor), "net",
1560
            "%s:%s:%s" % (family, lhost, lport),
1561
            "%s:%s:%s" % (family, rhost, rport), protocol,
1562
            "-A", "discard-zero-changes",
1563
            "-B", "consensus",
1564
            "--create-device",
1565
            ]
1566
    if dual_pri:
1567
      args.append("-m")
1568
    if hmac and secret:
1569
      args.extend(["-a", hmac, "-x", secret])
1570

    
1571
    if self.params[constants.LDP_NET_CUSTOM]:
1572
      args.extend(shlex.split(self.params[constants.LDP_NET_CUSTOM]))
1573

    
1574
    result = utils.RunCmd(args)
1575
    if result.failed:
1576
      _ThrowError("drbd%d: can't setup network: %s - %s",
1577
                  minor, result.fail_reason, result.output)
1578

    
1579
    def _CheckNetworkConfig():
1580
      info = self._GetDevInfo(self._GetShowData(minor))
1581
      if not "local_addr" in info or not "remote_addr" in info:
1582
        raise utils.RetryAgain()
1583

    
1584
      if (info["local_addr"] != (lhost, lport) or
1585
          info["remote_addr"] != (rhost, rport)):
1586
        raise utils.RetryAgain()
1587

    
1588
    try:
1589
      utils.Retry(_CheckNetworkConfig, 1.0, 10.0)
1590
    except utils.RetryTimeout:
1591
      _ThrowError("drbd%d: timeout while configuring network", minor)
1592

    
1593
  def AddChildren(self, devices):
1594
    """Add a disk to the DRBD device.
1595

1596
    """
1597
    if self.minor is None:
1598
      _ThrowError("drbd%d: can't attach to dbrd8 during AddChildren",
1599
                  self._aminor)
1600
    if len(devices) != 2:
1601
      _ThrowError("drbd%d: need two devices for AddChildren", self.minor)
1602
    info = self._GetDevInfo(self._GetShowData(self.minor))
1603
    if "local_dev" in info:
1604
      _ThrowError("drbd%d: already attached to a local disk", self.minor)
1605
    backend, meta = devices
1606
    if backend.dev_path is None or meta.dev_path is None:
1607
      _ThrowError("drbd%d: children not ready during AddChildren", self.minor)
1608
    backend.Open()
1609
    meta.Open()
1610
    self._CheckMetaSize(meta.dev_path)
1611
    self._InitMeta(self._FindUnusedMinor(), meta.dev_path)
1612

    
1613
    self._AssembleLocal(self.minor, backend.dev_path, meta.dev_path, self.size)
1614
    self._children = devices
1615

    
1616
  def RemoveChildren(self, devices):
1617
    """Detach the drbd device from local storage.
1618

1619
    """
1620
    if self.minor is None:
1621
      _ThrowError("drbd%d: can't attach to drbd8 during RemoveChildren",
1622
                  self._aminor)
1623
    # early return if we don't actually have backing storage
1624
    info = self._GetDevInfo(self._GetShowData(self.minor))
1625
    if "local_dev" not in info:
1626
      return
1627
    if len(self._children) != 2:
1628
      _ThrowError("drbd%d: we don't have two children: %s", self.minor,
1629
                  self._children)
1630
    if self._children.count(None) == 2: # we don't actually have children :)
1631
      logging.warning("drbd%d: requested detach while detached", self.minor)
1632
      return
1633
    if len(devices) != 2:
1634
      _ThrowError("drbd%d: we need two children in RemoveChildren", self.minor)
1635
    for child, dev in zip(self._children, devices):
1636
      if dev != child.dev_path:
1637
        _ThrowError("drbd%d: mismatch in local storage (%s != %s) in"
1638
                    " RemoveChildren", self.minor, dev, child.dev_path)
1639

    
1640
    self._ShutdownLocal(self.minor)
1641
    self._children = []
1642

    
1643
  @classmethod
1644
  def _SetMinorSyncParams(cls, minor, params):
1645
    """Set the parameters of the DRBD syncer.
1646

1647
    This is the low-level implementation.
1648

1649
    @type minor: int
1650
    @param minor: the drbd minor whose settings we change
1651
    @type params: dict
1652
    @param params: LD level disk parameters related to the synchronization
1653
    @rtype: list
1654
    @return: a list of error messages
1655

1656
    """
1657

    
1658
    args = ["drbdsetup", cls._DevPath(minor), "syncer"]
1659
    if params[constants.LDP_DYNAMIC_RESYNC]:
1660
      version = cls._GetVersion(cls._GetProcData())
1661
      vmin = version["k_minor"]
1662
      vrel = version["k_point"]
1663

    
1664
      # By definition we are using 8.x, so just check the rest of the version
1665
      # number
1666
      if vmin != 3 or vrel < 9:
1667
        msg = ("The current DRBD version (8.%d.%d) does not support the "
1668
               "dynamic resync speed controller" % (vmin, vrel))
1669
        logging.error(msg)
1670
        return [msg]
1671

    
1672
      if params[constants.LDP_PLAN_AHEAD] == 0:
1673
        msg = ("A value of 0 for c-plan-ahead disables the dynamic sync speed"
1674
               " controller at DRBD level. If you want to disable it, please"
1675
               " set the dynamic-resync disk parameter to False.")
1676
        logging.error(msg)
1677
        return [msg]
1678

    
1679
      # add the c-* parameters to args
1680
      args.extend(["--c-plan-ahead", params[constants.LDP_PLAN_AHEAD],
1681
                   "--c-fill-target", params[constants.LDP_FILL_TARGET],
1682
                   "--c-delay-target", params[constants.LDP_DELAY_TARGET],
1683
                   "--c-max-rate", params[constants.LDP_MAX_RATE],
1684
                   "--c-min-rate", params[constants.LDP_MIN_RATE],
1685
                   ])
1686

    
1687
    else:
1688
      args.extend(["-r", "%d" % params[constants.LDP_RESYNC_RATE]])
1689

    
1690
    args.append("--create-device")
1691
    result = utils.RunCmd(args)
1692
    if result.failed:
1693
      msg = ("Can't change syncer rate: %s - %s" %
1694
             (result.fail_reason, result.output))
1695
      logging.error(msg)
1696
      return [msg]
1697

    
1698
    return []
1699

    
1700
  def SetSyncParams(self, params):
1701
    """Set the synchronization parameters of the DRBD syncer.
1702

1703
    @type params: dict
1704
    @param params: LD level disk parameters related to the synchronization
1705
    @rtype: list
1706
    @return: a list of error messages, emitted both by the current node and by
1707
    children. An empty list means no errors
1708

1709
    """
1710
    if self.minor is None:
1711
      err = "Not attached during SetSyncParams"
1712
      logging.info(err)
1713
      return [err]
1714

    
1715
    children_result = super(DRBD8, self).SetSyncParams(params)
1716
    children_result.extend(self._SetMinorSyncParams(self.minor, params))
1717
    return children_result
1718

    
1719
  def PauseResumeSync(self, pause):
1720
    """Pauses or resumes the sync of a DRBD device.
1721

1722
    @param pause: Wether to pause or resume
1723
    @return: the success of the operation
1724

1725
    """
1726
    if self.minor is None:
1727
      logging.info("Not attached during PauseSync")
1728
      return False
1729

    
1730
    children_result = super(DRBD8, self).PauseResumeSync(pause)
1731

    
1732
    if pause:
1733
      cmd = "pause-sync"
1734
    else:
1735
      cmd = "resume-sync"
1736

    
1737
    result = utils.RunCmd(["drbdsetup", self.dev_path, cmd])
1738
    if result.failed:
1739
      logging.error("Can't %s: %s - %s", cmd,
1740
                    result.fail_reason, result.output)
1741
    return not result.failed and children_result
1742

    
1743
  def GetProcStatus(self):
1744
    """Return device data from /proc.
1745

1746
    """
1747
    if self.minor is None:
1748
      _ThrowError("drbd%d: GetStats() called while not attached", self._aminor)
1749
    proc_info = self._MassageProcData(self._GetProcData())
1750
    if self.minor not in proc_info:
1751
      _ThrowError("drbd%d: can't find myself in /proc", self.minor)
1752
    return DRBD8Status(proc_info[self.minor])
1753

    
1754
  def GetSyncStatus(self):
1755
    """Returns the sync status of the device.
1756

1757

1758
    If sync_percent is None, it means all is ok
1759
    If estimated_time is None, it means we can't estimate
1760
    the time needed, otherwise it's the time left in seconds.
1761

1762

1763
    We set the is_degraded parameter to True on two conditions:
1764
    network not connected or local disk missing.
1765

1766
    We compute the ldisk parameter based on whether we have a local
1767
    disk or not.
1768

1769
    @rtype: objects.BlockDevStatus
1770

1771
    """
1772
    if self.minor is None and not self.Attach():
1773
      _ThrowError("drbd%d: can't Attach() in GetSyncStatus", self._aminor)
1774

    
1775
    stats = self.GetProcStatus()
1776
    is_degraded = not stats.is_connected or not stats.is_disk_uptodate
1777

    
1778
    if stats.is_disk_uptodate:
1779
      ldisk_status = constants.LDS_OKAY
1780
    elif stats.is_diskless:
1781
      ldisk_status = constants.LDS_FAULTY
1782
    else:
1783
      ldisk_status = constants.LDS_UNKNOWN
1784

    
1785
    return objects.BlockDevStatus(dev_path=self.dev_path,
1786
                                  major=self.major,
1787
                                  minor=self.minor,
1788
                                  sync_percent=stats.sync_percent,
1789
                                  estimated_time=stats.est_time,
1790
                                  is_degraded=is_degraded,
1791
                                  ldisk_status=ldisk_status)
1792

    
1793
  def Open(self, force=False):
1794
    """Make the local state primary.
1795

1796
    If the 'force' parameter is given, the '-o' option is passed to
1797
    drbdsetup. Since this is a potentially dangerous operation, the
1798
    force flag should be only given after creation, when it actually
1799
    is mandatory.
1800

1801
    """
1802
    if self.minor is None and not self.Attach():
1803
      logging.error("DRBD cannot attach to a device during open")
1804
      return False
1805
    cmd = ["drbdsetup", self.dev_path, "primary"]
1806
    if force:
1807
      cmd.append("-o")
1808
    result = utils.RunCmd(cmd)
1809
    if result.failed:
1810
      _ThrowError("drbd%d: can't make drbd device primary: %s", self.minor,
1811
                  result.output)
1812

    
1813
  def Close(self):
1814
    """Make the local state secondary.
1815

1816
    This will, of course, fail if the device is in use.
1817

1818
    """
1819
    if self.minor is None and not self.Attach():
1820
      _ThrowError("drbd%d: can't Attach() in Close()", self._aminor)
1821
    result = utils.RunCmd(["drbdsetup", self.dev_path, "secondary"])
1822
    if result.failed:
1823
      _ThrowError("drbd%d: can't switch drbd device to secondary: %s",
1824
                  self.minor, result.output)
1825

    
1826
  def DisconnectNet(self):
1827
    """Removes network configuration.
1828

1829
    This method shutdowns the network side of the device.
1830

1831
    The method will wait up to a hardcoded timeout for the device to
1832
    go into standalone after the 'disconnect' command before
1833
    re-configuring it, as sometimes it takes a while for the
1834
    disconnect to actually propagate and thus we might issue a 'net'
1835
    command while the device is still connected. If the device will
1836
    still be attached to the network and we time out, we raise an
1837
    exception.
1838

1839
    """
1840
    if self.minor is None:
1841
      _ThrowError("drbd%d: disk not attached in re-attach net", self._aminor)
1842

    
1843
    if None in (self._lhost, self._lport, self._rhost, self._rport):
1844
      _ThrowError("drbd%d: DRBD disk missing network info in"
1845
                  " DisconnectNet()", self.minor)
1846

    
1847
    class _DisconnectStatus:
1848
      def __init__(self, ever_disconnected):
1849
        self.ever_disconnected = ever_disconnected
1850

    
1851
    dstatus = _DisconnectStatus(_IgnoreError(self._ShutdownNet, self.minor))
1852

    
1853
    def _WaitForDisconnect():
1854
      if self.GetProcStatus().is_standalone:
1855
        return
1856

    
1857
      # retry the disconnect, it seems possible that due to a well-time
1858
      # disconnect on the peer, my disconnect command might be ignored and
1859
      # forgotten
1860
      dstatus.ever_disconnected = \
1861
        _IgnoreError(self._ShutdownNet, self.minor) or dstatus.ever_disconnected
1862

    
1863
      raise utils.RetryAgain()
1864

    
1865
    # Keep start time
1866
    start_time = time.time()
1867

    
1868
    try:
1869
      # Start delay at 100 milliseconds and grow up to 2 seconds
1870
      utils.Retry(_WaitForDisconnect, (0.1, 1.5, 2.0),
1871
                  self._NET_RECONFIG_TIMEOUT)
1872
    except utils.RetryTimeout:
1873
      if dstatus.ever_disconnected:
1874
        msg = ("drbd%d: device did not react to the"
1875
               " 'disconnect' command in a timely manner")
1876
      else:
1877
        msg = "drbd%d: can't shutdown network, even after multiple retries"
1878

    
1879
      _ThrowError(msg, self.minor)
1880

    
1881
    reconfig_time = time.time() - start_time
1882
    if reconfig_time > (self._NET_RECONFIG_TIMEOUT * 0.25):
1883
      logging.info("drbd%d: DisconnectNet: detach took %.3f seconds",
1884
                   self.minor, reconfig_time)
1885

    
1886
  def AttachNet(self, multimaster):
1887
    """Reconnects the network.
1888

1889
    This method connects the network side of the device with a
1890
    specified multi-master flag. The device needs to be 'Standalone'
1891
    but have valid network configuration data.
1892

1893
    Args:
1894
      - multimaster: init the network in dual-primary mode
1895

1896
    """
1897
    if self.minor is None:
1898
      _ThrowError("drbd%d: device not attached in AttachNet", self._aminor)
1899

    
1900
    if None in (self._lhost, self._lport, self._rhost, self._rport):
1901
      _ThrowError("drbd%d: missing network info in AttachNet()", self.minor)
1902

    
1903
    status = self.GetProcStatus()
1904

    
1905
    if not status.is_standalone:
1906
      _ThrowError("drbd%d: device is not standalone in AttachNet", self.minor)
1907

    
1908
    self._AssembleNet(self.minor,
1909
                      (self._lhost, self._lport, self._rhost, self._rport),
1910
                      constants.DRBD_NET_PROTOCOL, dual_pri=multimaster,
1911
                      hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1912

    
1913
  def Attach(self):
1914
    """Check if our minor is configured.
1915

1916
    This doesn't do any device configurations - it only checks if the
1917
    minor is in a state different from Unconfigured.
1918

1919
    Note that this function will not change the state of the system in
1920
    any way (except in case of side-effects caused by reading from
1921
    /proc).
1922

1923
    """
1924
    used_devs = self.GetUsedDevs()
1925
    if self._aminor in used_devs:
1926
      minor = self._aminor
1927
    else:
1928
      minor = None
1929

    
1930
    self._SetFromMinor(minor)
1931
    return minor is not None
1932

    
1933
  def Assemble(self):
1934
    """Assemble the drbd.
1935

1936
    Method:
1937
      - if we have a configured device, we try to ensure that it matches
1938
        our config
1939
      - if not, we create it from zero
1940
      - anyway, set the device parameters
1941

1942
    """
1943
    super(DRBD8, self).Assemble()
1944

    
1945
    self.Attach()
1946
    if self.minor is None:
1947
      # local device completely unconfigured
1948
      self._FastAssemble()
1949
    else:
1950
      # we have to recheck the local and network status and try to fix
1951
      # the device
1952
      self._SlowAssemble()
1953

    
1954
    sync_errors = self.SetSyncParams(self.params)
1955
    if sync_errors:
1956
      _ThrowError("drbd%d: can't set the synchronization parameters: %s" %
1957
                  (self.minor, utils.CommaJoin(sync_errors)))
1958

    
1959
  def _SlowAssemble(self):
1960
    """Assembles the DRBD device from a (partially) configured device.
1961

1962
    In case of partially attached (local device matches but no network
1963
    setup), we perform the network attach. If successful, we re-test
1964
    the attach if can return success.
1965

1966
    """
1967
    # TODO: Rewrite to not use a for loop just because there is 'break'
1968
    # pylint: disable=W0631
1969
    net_data = (self._lhost, self._lport, self._rhost, self._rport)
1970
    for minor in (self._aminor,):
1971
      info = self._GetDevInfo(self._GetShowData(minor))
1972
      match_l = self._MatchesLocal(info)
1973
      match_r = self._MatchesNet(info)
1974

    
1975
      if match_l and match_r:
1976
        # everything matches
1977
        break
1978

    
1979
      if match_l and not match_r and "local_addr" not in info:
1980
        # disk matches, but not attached to network, attach and recheck
1981
        self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
1982
                          hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1983
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1984
          break
1985
        else:
1986
          _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
1987
                      " show' disagrees", minor)
1988

    
1989
      if match_r and "local_dev" not in info:
1990
        # no local disk, but network attached and it matches
1991
        self._AssembleLocal(minor, self._children[0].dev_path,
1992
                            self._children[1].dev_path, self.size)
1993
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1994
          break
1995
        else:
1996
          _ThrowError("drbd%d: disk attach successful, but 'drbdsetup"
1997
                      " show' disagrees", minor)
1998

    
1999
      # this case must be considered only if we actually have local
2000
      # storage, i.e. not in diskless mode, because all diskless
2001
      # devices are equal from the point of view of local
2002
      # configuration
2003
      if (match_l and "local_dev" in info and
2004
          not match_r and "local_addr" in info):
2005
        # strange case - the device network part points to somewhere
2006
        # else, even though its local storage is ours; as we own the
2007
        # drbd space, we try to disconnect from the remote peer and
2008
        # reconnect to our correct one
2009
        try:
2010
          self._ShutdownNet(minor)
2011
        except errors.BlockDeviceError, err:
2012
          _ThrowError("drbd%d: device has correct local storage, wrong"
2013
                      " remote peer and is unable to disconnect in order"
2014
                      " to attach to the correct peer: %s", minor, str(err))
2015
        # note: _AssembleNet also handles the case when we don't want
2016
        # local storage (i.e. one or more of the _[lr](host|port) is
2017
        # None)
2018
        self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
2019
                          hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
2020
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
2021
          break
2022
        else:
2023
          _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
2024
                      " show' disagrees", minor)
2025

    
2026
    else:
2027
      minor = None
2028

    
2029
    self._SetFromMinor(minor)
2030
    if minor is None:
2031
      _ThrowError("drbd%d: cannot activate, unknown or unhandled reason",
2032
                  self._aminor)
2033

    
2034
  def _FastAssemble(self):
2035
    """Assemble the drbd device from zero.
2036

2037
    This is run when in Assemble we detect our minor is unused.
2038

2039
    """
2040
    minor = self._aminor
2041
    if self._children and self._children[0] and self._children[1]:
2042
      self._AssembleLocal(minor, self._children[0].dev_path,
2043
                          self._children[1].dev_path, self.size)
2044
    if self._lhost and self._lport and self._rhost and self._rport:
2045
      self._AssembleNet(minor,
2046
                        (self._lhost, self._lport, self._rhost, self._rport),
2047
                        constants.DRBD_NET_PROTOCOL,
2048
                        hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
2049
    self._SetFromMinor(minor)
2050

    
2051
  @classmethod
2052
  def _ShutdownLocal(cls, minor):
2053
    """Detach from the local device.
2054

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

2058
    """
2059
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "detach"])
2060
    if result.failed:
2061
      _ThrowError("drbd%d: can't detach local disk: %s", minor, result.output)
2062

    
2063
  @classmethod
2064
  def _ShutdownNet(cls, minor):
2065
    """Disconnect from the remote peer.
2066

2067
    This fails if we don't have a local device.
2068

2069
    """
2070
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disconnect"])
2071
    if result.failed:
2072
      _ThrowError("drbd%d: can't shutdown network: %s", minor, result.output)
2073

    
2074
  @classmethod
2075
  def _ShutdownAll(cls, minor):
2076
    """Deactivate the device.
2077

2078
    This will, of course, fail if the device is in use.
2079

2080
    """
2081
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "down"])
2082
    if result.failed:
2083
      _ThrowError("drbd%d: can't shutdown drbd device: %s",
2084
                  minor, result.output)
2085

    
2086
  def Shutdown(self):
2087
    """Shutdown the DRBD device.
2088

2089
    """
2090
    if self.minor is None and not self.Attach():
2091
      logging.info("drbd%d: not attached during Shutdown()", self._aminor)
2092
      return
2093
    minor = self.minor
2094
    self.minor = None
2095
    self.dev_path = None
2096
    self._ShutdownAll(minor)
2097

    
2098
  def Remove(self):
2099
    """Stub remove for DRBD devices.
2100

2101
    """
2102
    self.Shutdown()
2103

    
2104
  @classmethod
2105
  def Create(cls, unique_id, children, size, params):
2106
    """Create a new DRBD8 device.
2107

2108
    Since DRBD devices are not created per se, just assembled, this
2109
    function only initializes the metadata.
2110

2111
    """
2112
    if len(children) != 2:
2113
      raise errors.ProgrammerError("Invalid setup for the drbd device")
2114
    # check that the minor is unused
2115
    aminor = unique_id[4]
2116
    proc_info = cls._MassageProcData(cls._GetProcData())
2117
    if aminor in proc_info:
2118
      status = DRBD8Status(proc_info[aminor])
2119
      in_use = status.is_in_use
2120
    else:
2121
      in_use = False
2122
    if in_use:
2123
      _ThrowError("drbd%d: minor is already in use at Create() time", aminor)
2124
    meta = children[1]
2125
    meta.Assemble()
2126
    if not meta.Attach():
2127
      _ThrowError("drbd%d: can't attach to meta device '%s'",
2128
                  aminor, meta)
2129
    cls._CheckMetaSize(meta.dev_path)
2130
    cls._InitMeta(aminor, meta.dev_path)
2131
    return cls(unique_id, children, size, params)
2132

    
2133
  def Grow(self, amount, dryrun, backingstore):
2134
    """Resize the DRBD device and its backing storage.
2135

2136
    """
2137
    if self.minor is None:
2138
      _ThrowError("drbd%d: Grow called while not attached", self._aminor)
2139
    if len(self._children) != 2 or None in self._children:
2140
      _ThrowError("drbd%d: cannot grow diskless device", self.minor)
2141
    self._children[0].Grow(amount, dryrun, backingstore)
2142
    if dryrun or backingstore:
2143
      # DRBD does not support dry-run mode and is not backing storage,
2144
      # so we'll return here
2145
      return
2146
    result = utils.RunCmd(["drbdsetup", self.dev_path, "resize", "-s",
2147
                           "%dm" % (self.size + amount)])
2148
    if result.failed:
2149
      _ThrowError("drbd%d: resize failed: %s", self.minor, result.output)
2150

    
2151

    
2152
class FileStorage(BlockDev):
2153
  """File device.
2154

2155
  This class represents the a file storage backend device.
2156

2157
  The unique_id for the file device is a (file_driver, file_path) tuple.
2158

2159
  """
2160
  def __init__(self, unique_id, children, size, params):
2161
    """Initalizes a file device backend.
2162

2163
    """
2164
    if children:
2165
      raise errors.BlockDeviceError("Invalid setup for file device")
2166
    super(FileStorage, self).__init__(unique_id, children, size, params)
2167
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
2168
      raise ValueError("Invalid configuration data %s" % str(unique_id))
2169
    self.driver = unique_id[0]
2170
    self.dev_path = unique_id[1]
2171
    self.Attach()
2172

    
2173
  def Assemble(self):
2174
    """Assemble the device.
2175

2176
    Checks whether the file device exists, raises BlockDeviceError otherwise.
2177

2178
    """
2179
    if not os.path.exists(self.dev_path):
2180
      _ThrowError("File device '%s' does not exist" % self.dev_path)
2181

    
2182
  def Shutdown(self):
2183
    """Shutdown the device.
2184

2185
    This is a no-op for the file type, as we don't deactivate
2186
    the file on shutdown.
2187

2188
    """
2189
    pass
2190

    
2191
  def Open(self, force=False):
2192
    """Make the device ready for I/O.
2193

2194
    This is a no-op for the file type.
2195

2196
    """
2197
    pass
2198

    
2199
  def Close(self):
2200
    """Notifies that the device will no longer be used for I/O.
2201

2202
    This is a no-op for the file type.
2203

2204
    """
2205
    pass
2206

    
2207
  def Remove(self):
2208
    """Remove the file backing the block device.
2209

2210
    @rtype: boolean
2211
    @return: True if the removal was successful
2212

2213
    """
2214
    try:
2215
      os.remove(self.dev_path)
2216
    except OSError, err:
2217
      if err.errno != errno.ENOENT:
2218
        _ThrowError("Can't remove file '%s': %s", self.dev_path, err)
2219

    
2220
  def Rename(self, new_id):
2221
    """Renames the file.
2222

2223
    """
2224
    # TODO: implement rename for file-based storage
2225
    _ThrowError("Rename is not supported for file-based storage")
2226

    
2227
  def Grow(self, amount, dryrun, backingstore):
2228
    """Grow the file
2229

2230
    @param amount: the amount (in mebibytes) to grow with
2231

2232
    """
2233
    if not backingstore:
2234
      return
2235
    # Check that the file exists
2236
    self.Assemble()
2237
    current_size = self.GetActualSize()
2238
    new_size = current_size + amount * 1024 * 1024
2239
    assert new_size > current_size, "Cannot Grow with a negative amount"
2240
    # We can't really simulate the growth
2241
    if dryrun:
2242
      return
2243
    try:
2244
      f = open(self.dev_path, "a+")
2245
      f.truncate(new_size)
2246
      f.close()
2247
    except EnvironmentError, err:
2248
      _ThrowError("Error in file growth: %", str(err))
2249

    
2250
  def Attach(self):
2251
    """Attach to an existing file.
2252

2253
    Check if this file already exists.
2254

2255
    @rtype: boolean
2256
    @return: True if file exists
2257

2258
    """
2259
    self.attached = os.path.exists(self.dev_path)
2260
    return self.attached
2261

    
2262
  def GetActualSize(self):
2263
    """Return the actual disk size.
2264

2265
    @note: the device needs to be active when this is called
2266

2267
    """
2268
    assert self.attached, "BlockDevice not attached in GetActualSize()"
2269
    try:
2270
      st = os.stat(self.dev_path)
2271
      return st.st_size
2272
    except OSError, err:
2273
      _ThrowError("Can't stat %s: %s", self.dev_path, err)
2274

    
2275
  @classmethod
2276
  def Create(cls, unique_id, children, size, params):
2277
    """Create a new file.
2278

2279
    @param size: the size of file in MiB
2280

2281
    @rtype: L{bdev.FileStorage}
2282
    @return: an instance of FileStorage
2283

2284
    """
2285
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
2286
      raise ValueError("Invalid configuration data %s" % str(unique_id))
2287
    dev_path = unique_id[1]
2288
    try:
2289
      fd = os.open(dev_path, os.O_RDWR | os.O_CREAT | os.O_EXCL)
2290
      f = os.fdopen(fd, "w")
2291
      f.truncate(size * 1024 * 1024)
2292
      f.close()
2293
    except EnvironmentError, err:
2294
      if err.errno == errno.EEXIST:
2295
        _ThrowError("File already existing: %s", dev_path)
2296
      _ThrowError("Error in file creation: %", str(err))
2297

    
2298
    return FileStorage(unique_id, children, size, params)
2299

    
2300

    
2301
class PersistentBlockDevice(BlockDev):
2302
  """A block device with persistent node
2303

2304
  May be either directly attached, or exposed through DM (e.g. dm-multipath).
2305
  udev helpers are probably required to give persistent, human-friendly
2306
  names.
2307

2308
  For the time being, pathnames are required to lie under /dev.
2309

2310
  """
2311
  def __init__(self, unique_id, children, size, params):
2312
    """Attaches to a static block device.
2313

2314
    The unique_id is a path under /dev.
2315

2316
    """
2317
    super(PersistentBlockDevice, self).__init__(unique_id, children, size,
2318
                                                params)
2319
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
2320
      raise ValueError("Invalid configuration data %s" % str(unique_id))
2321
    self.dev_path = unique_id[1]
2322
    if not os.path.realpath(self.dev_path).startswith("/dev/"):
2323
      raise ValueError("Full path '%s' lies outside /dev" %
2324
                              os.path.realpath(self.dev_path))
2325
    # TODO: this is just a safety guard checking that we only deal with devices
2326
    # we know how to handle. In the future this will be integrated with
2327
    # external storage backends and possible values will probably be collected
2328
    # from the cluster configuration.
2329
    if unique_id[0] != constants.BLOCKDEV_DRIVER_MANUAL:
2330
      raise ValueError("Got persistent block device of invalid type: %s" %
2331
                       unique_id[0])
2332

    
2333
    self.major = self.minor = None
2334
    self.Attach()
2335

    
2336
  @classmethod
2337
  def Create(cls, unique_id, children, size, params):
2338
    """Create a new device
2339

2340
    This is a noop, we only return a PersistentBlockDevice instance
2341

2342
    """
2343
    return PersistentBlockDevice(unique_id, children, 0, params)
2344

    
2345
  def Remove(self):
2346
    """Remove a device
2347

2348
    This is a noop
2349

2350
    """
2351
    pass
2352

    
2353
  def Rename(self, new_id):
2354
    """Rename this device.
2355

2356
    """
2357
    _ThrowError("Rename is not supported for PersistentBlockDev storage")
2358

    
2359
  def Attach(self):
2360
    """Attach to an existing block device.
2361

2362

2363
    """
2364
    self.attached = False
2365
    try:
2366
      st = os.stat(self.dev_path)
2367
    except OSError, err:
2368
      logging.error("Error stat()'ing %s: %s", self.dev_path, str(err))
2369
      return False
2370

    
2371
    if not stat.S_ISBLK(st.st_mode):
2372
      logging.error("%s is not a block device", self.dev_path)
2373
      return False
2374

    
2375
    self.major = os.major(st.st_rdev)
2376
    self.minor = os.minor(st.st_rdev)
2377
    self.attached = True
2378

    
2379
    return True
2380

    
2381
  def Assemble(self):
2382
    """Assemble the device.
2383

2384
    """
2385
    pass
2386

    
2387
  def Shutdown(self):
2388
    """Shutdown the device.
2389

2390
    """
2391
    pass
2392

    
2393
  def Open(self, force=False):
2394
    """Make the device ready for I/O.
2395

2396
    """
2397
    pass
2398

    
2399
  def Close(self):
2400
    """Notifies that the device will no longer be used for I/O.
2401

2402
    """
2403
    pass
2404

    
2405
  def Grow(self, amount, dryrun, backingstore):
2406
    """Grow the logical volume.
2407

2408
    """
2409
    _ThrowError("Grow is not supported for PersistentBlockDev storage")
2410

    
2411

    
2412
class RADOSBlockDevice(BlockDev):
2413
  """A RADOS Block Device (rbd).
2414

2415
  This class implements the RADOS Block Device for the backend. You need
2416
  the rbd kernel driver, the RADOS Tools and a working RADOS cluster for
2417
  this to be functional.
2418

2419
  """
2420
  def __init__(self, unique_id, children, size, params):
2421
    """Attaches to an rbd device.
2422

2423
    """
2424
    super(RADOSBlockDevice, self).__init__(unique_id, children, size, params)
2425
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
2426
      raise ValueError("Invalid configuration data %s" % str(unique_id))
2427

    
2428
    self.driver, self.rbd_name = unique_id
2429

    
2430
    self.major = self.minor = None
2431
    self.Attach()
2432

    
2433
  @classmethod
2434
  def Create(cls, unique_id, children, size, params):
2435
    """Create a new rbd device.
2436

2437
    Provision a new rbd volume inside a RADOS pool.
2438

2439
    """
2440
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
2441
      raise errors.ProgrammerError("Invalid configuration data %s" %
2442
                                   str(unique_id))
2443
    rbd_pool = params[constants.LDP_POOL]
2444
    rbd_name = unique_id[1]
2445

    
2446
    # Provision a new rbd volume (Image) inside the RADOS cluster.
2447
    cmd = [constants.RBD_CMD, "create", "-p", rbd_pool,
2448
           rbd_name, "--size", "%s" % size]
2449
    result = utils.RunCmd(cmd)
2450
    if result.failed:
2451
      _ThrowError("rbd creation failed (%s): %s",
2452
                  result.fail_reason, result.output)
2453

    
2454
    return RADOSBlockDevice(unique_id, children, size, params)
2455

    
2456
  def Remove(self):
2457
    """Remove the rbd device.
2458

2459
    """
2460
    rbd_pool = self.params[constants.LDP_POOL]
2461
    rbd_name = self.unique_id[1]
2462

    
2463
    if not self.minor and not self.Attach():
2464
      # The rbd device doesn't exist.
2465
      return
2466

    
2467
    # First shutdown the device (remove mappings).
2468
    self.Shutdown()
2469

    
2470
    # Remove the actual Volume (Image) from the RADOS cluster.
2471
    cmd = [constants.RBD_CMD, "rm", "-p", rbd_pool, rbd_name]
2472
    result = utils.RunCmd(cmd)
2473
    if result.failed:
2474
      _ThrowError("Can't remove Volume from cluster with rbd rm: %s - %s",
2475
                  result.fail_reason, result.output)
2476

    
2477
  def Rename(self, new_id):
2478
    """Rename this device.
2479

2480
    """
2481
    pass
2482

    
2483
  def Attach(self):
2484
    """Attach to an existing rbd device.
2485

2486
    This method maps the rbd volume that matches our name with
2487
    an rbd device and then attaches to this device.
2488

2489
    """
2490
    self.attached = False
2491

    
2492
    # Map the rbd volume to a block device under /dev
2493
    self.dev_path = self._MapVolumeToBlockdev(self.unique_id)
2494

    
2495
    try:
2496
      st = os.stat(self.dev_path)
2497
    except OSError, err:
2498
      logging.error("Error stat()'ing %s: %s", self.dev_path, str(err))
2499
      return False
2500

    
2501
    if not stat.S_ISBLK(st.st_mode):
2502
      logging.error("%s is not a block device", self.dev_path)
2503
      return False
2504

    
2505
    self.major = os.major(st.st_rdev)
2506
    self.minor = os.minor(st.st_rdev)
2507
    self.attached = True
2508

    
2509
    return True
2510

    
2511
  def _MapVolumeToBlockdev(self, unique_id):
2512
    """Maps existing rbd volumes to block devices.
2513

2514
    This method should be idempotent if the mapping already exists.
2515

2516
    @rtype: string
2517
    @return: the block device path that corresponds to the volume
2518

2519
    """
2520
    pool = self.params[constants.LDP_POOL]
2521
    name = unique_id[1]
2522

    
2523
    # Check if the mapping already exists.
2524
    showmap_cmd = [constants.RBD_CMD, "showmapped", "-p", pool]
2525
    result = utils.RunCmd(showmap_cmd)
2526
    if result.failed:
2527
      _ThrowError("rbd showmapped failed (%s): %s",
2528
                  result.fail_reason, result.output)
2529

    
2530
    rbd_dev = self._ParseRbdShowmappedOutput(result.output, name)
2531

    
2532
    if rbd_dev:
2533
      # The mapping exists. Return it.
2534
      return rbd_dev
2535

    
2536
    # The mapping doesn't exist. Create it.
2537
    map_cmd = [constants.RBD_CMD, "map", "-p", pool, name]
2538
    result = utils.RunCmd(map_cmd)
2539
    if result.failed:
2540
      _ThrowError("rbd map failed (%s): %s",
2541
                  result.fail_reason, result.output)
2542

    
2543
    # Find the corresponding rbd device.
2544
    showmap_cmd = [constants.RBD_CMD, "showmapped", "-p", pool]
2545
    result = utils.RunCmd(showmap_cmd)
2546
    if result.failed:
2547
      _ThrowError("rbd map succeeded, but showmapped failed (%s): %s",
2548
                  result.fail_reason, result.output)
2549

    
2550
    rbd_dev = self._ParseRbdShowmappedOutput(result.output, name)
2551

    
2552
    if not rbd_dev:
2553
      _ThrowError("rbd map succeeded, but could not find the rbd block"
2554
                  " device in output of showmapped, for volume: %s", name)
2555

    
2556
    # The device was successfully mapped. Return it.
2557
    return rbd_dev
2558

    
2559
  @staticmethod
2560
  def _ParseRbdShowmappedOutput(output, volume_name):
2561
    """Parse the output of `rbd showmapped'.
2562

2563
    This method parses the output of `rbd showmapped' and returns
2564
    the rbd block device path (e.g. /dev/rbd0) that matches the
2565
    given rbd volume.
2566

2567
    @type output: string
2568
    @param output: the whole output of `rbd showmapped'
2569
    @type volume_name: string
2570
    @param volume_name: the name of the volume whose device we search for
2571
    @rtype: string or None
2572
    @return: block device path if the volume is mapped, else None
2573

2574
    """
2575
    allfields = 5
2576
    volumefield = 2
2577
    devicefield = 4
2578

    
2579
    field_sep = "\t"
2580

    
2581
    lines = output.splitlines()
2582
    splitted_lines = map(lambda l: l.split(field_sep), lines)
2583

    
2584
    # Check empty output.
2585
    if not splitted_lines:
2586
      _ThrowError("rbd showmapped returned empty output")
2587

    
2588
    # Check showmapped header line, to determine number of fields.
2589
    field_cnt = len(splitted_lines[0])
2590
    if field_cnt != allfields:
2591
      _ThrowError("Cannot parse rbd showmapped output because its format"
2592
                  " seems to have changed; expected %s fields, found %s",
2593
                  allfields, field_cnt)
2594

    
2595
    matched_lines = \
2596
      filter(lambda l: len(l) == allfields and l[volumefield] == volume_name,
2597
             splitted_lines)
2598

    
2599
    if len(matched_lines) > 1:
2600
      _ThrowError("The rbd volume %s is mapped more than once."
2601
                  " This shouldn't happen, try to unmap the extra"
2602
                  " devices manually.", volume_name)
2603

    
2604
    if matched_lines:
2605
      # rbd block device found. Return it.
2606
      rbd_dev = matched_lines[0][devicefield]
2607
      return rbd_dev
2608

    
2609
    # The given volume is not mapped.
2610
    return None
2611

    
2612
  def Assemble(self):
2613
    """Assemble the device.
2614

2615
    """
2616
    pass
2617

    
2618
  def Shutdown(self):
2619
    """Shutdown the device.
2620

2621
    """
2622
    if not self.minor and not self.Attach():
2623
      # The rbd device doesn't exist.
2624
      return
2625

    
2626
    # Unmap the block device from the Volume.
2627
    self._UnmapVolumeFromBlockdev(self.unique_id)
2628

    
2629
    self.minor = None
2630
    self.dev_path = None
2631

    
2632
  def _UnmapVolumeFromBlockdev(self, unique_id):
2633
    """Unmaps the rbd device from the Volume it is mapped.
2634

2635
    Unmaps the rbd device from the Volume it was previously mapped to.
2636
    This method should be idempotent if the Volume isn't mapped.
2637

2638
    """
2639
    pool = self.params[constants.LDP_POOL]
2640
    name = unique_id[1]
2641

    
2642
    # Check if the mapping already exists.
2643
    showmap_cmd = [constants.RBD_CMD, "showmapped", "-p", pool]
2644
    result = utils.RunCmd(showmap_cmd)
2645
    if result.failed:
2646
      _ThrowError("rbd showmapped failed [during unmap](%s): %s",
2647
                  result.fail_reason, result.output)
2648

    
2649
    rbd_dev = self._ParseRbdShowmappedOutput(result.output, name)
2650

    
2651
    if rbd_dev:
2652
      # The mapping exists. Unmap the rbd device.
2653
      unmap_cmd = [constants.RBD_CMD, "unmap", "%s" % rbd_dev]
2654
      result = utils.RunCmd(unmap_cmd)
2655
      if result.failed:
2656
        _ThrowError("rbd unmap failed (%s): %s",
2657
                    result.fail_reason, result.output)
2658

    
2659
  def Open(self, force=False):
2660
    """Make the device ready for I/O.
2661

2662
    """
2663
    pass
2664

    
2665
  def Close(self):
2666
    """Notifies that the device will no longer be used for I/O.
2667

2668
    """
2669
    pass
2670

    
2671
  def Grow(self, amount, dryrun, backingstore):
2672
    """Grow the Volume.
2673

2674
    @type amount: integer
2675
    @param amount: the amount (in mebibytes) to grow with
2676
    @type dryrun: boolean
2677
    @param dryrun: whether to execute the operation in simulation mode
2678
        only, without actually increasing the size
2679

2680
    """
2681
    if not backingstore:
2682
      return
2683
    if not self.Attach():
2684
      _ThrowError("Can't attach to rbd device during Grow()")
2685

    
2686
    if dryrun:
2687
      # the rbd tool does not support dry runs of resize operations.
2688
      # Since rbd volumes are thinly provisioned, we assume
2689
      # there is always enough free space for the operation.
2690
      return
2691

    
2692
    rbd_pool = self.params[constants.LDP_POOL]
2693
    rbd_name = self.unique_id[1]
2694
    new_size = self.size + amount
2695

    
2696
    # Resize the rbd volume (Image) inside the RADOS cluster.
2697
    cmd = [constants.RBD_CMD, "resize", "-p", rbd_pool,
2698
           rbd_name, "--size", "%s" % new_size]
2699
    result = utils.RunCmd(cmd)
2700
    if result.failed:
2701
      _ThrowError("rbd resize failed (%s): %s",
2702
                  result.fail_reason, result.output)
2703

    
2704

    
2705
DEV_MAP = {
2706
  constants.LD_LV: LogicalVolume,
2707
  constants.LD_DRBD8: DRBD8,
2708
  constants.LD_BLOCKDEV: PersistentBlockDevice,
2709
  constants.LD_RBD: RADOSBlockDevice,
2710
  }
2711

    
2712
if constants.ENABLE_FILE_STORAGE or constants.ENABLE_SHARED_FILE_STORAGE:
2713
  DEV_MAP[constants.LD_FILE] = FileStorage
2714

    
2715

    
2716
def _VerifyDiskType(dev_type):
2717
  if dev_type not in DEV_MAP:
2718
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
2719

    
2720

    
2721
def _VerifyDiskParams(disk):
2722
  """Verifies if all disk parameters are set.
2723

2724
  """
2725
  missing = set(constants.DISK_LD_DEFAULTS[disk.dev_type]) - set(disk.params)
2726
  if missing:
2727
    raise errors.ProgrammerError("Block device is missing disk parameters: %s" %
2728
                                 missing)
2729

    
2730

    
2731
def FindDevice(disk, children):
2732
  """Search for an existing, assembled device.
2733

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

2737
  @type disk: L{objects.Disk}
2738
  @param disk: the disk object to find
2739
  @type children: list of L{bdev.BlockDev}
2740
  @param children: the list of block devices that are children of the device
2741
                  represented by the disk parameter
2742

2743
  """
2744
  _VerifyDiskType(disk.dev_type)
2745
  device = DEV_MAP[disk.dev_type](disk.physical_id, children, disk.size,
2746
                                  disk.params)
2747
  if not device.attached:
2748
    return None
2749
  return device
2750

    
2751

    
2752
def Assemble(disk, children):
2753
  """Try to attach or assemble an existing device.
2754

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

2758
  @type disk: L{objects.Disk}
2759
  @param disk: the disk object to assemble
2760
  @type children: list of L{bdev.BlockDev}
2761
  @param children: the list of block devices that are children of the device
2762
                  represented by the disk parameter
2763

2764
  """
2765
  _VerifyDiskType(disk.dev_type)
2766
  _VerifyDiskParams(disk)
2767
  device = DEV_MAP[disk.dev_type](disk.physical_id, children, disk.size,
2768
                                  disk.params)
2769
  device.Assemble()
2770
  return device
2771

    
2772

    
2773
def Create(disk, children):
2774
  """Create a device.
2775

2776
  @type disk: L{objects.Disk}
2777
  @param disk: the disk object to create
2778
  @type children: list of L{bdev.BlockDev}
2779
  @param children: the list of block devices that are children of the device
2780
                  represented by the disk parameter
2781

2782
  """
2783
  _VerifyDiskType(disk.dev_type)
2784
  _VerifyDiskParams(disk)
2785
  device = DEV_MAP[disk.dev_type].Create(disk.physical_id, children, disk.size,
2786
                                         disk.params)
2787
  return device