Statistics
| Branch: | Tag: | Revision:

root / lib / bdev.py @ 8a6dbce8

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
class BlockDev(object):
145
  """Block device abstract class.
146

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

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

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

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

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

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

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

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

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

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

208
    """
209
    pass
210

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

214
    """
215
    raise NotImplementedError
216

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

220
    """
221
    raise NotImplementedError
222

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

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

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

234
    """
235
    raise NotImplementedError
236

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

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

244
    """
245
    raise NotImplementedError
246

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

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

252
    """
253
    raise NotImplementedError
254

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

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

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

264
    """
265
    raise NotImplementedError
266

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

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

274
    """
275
    raise NotImplementedError
276

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

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

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

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

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

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

300
    @param pause: Whether to pause or resume
301

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

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

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

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

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

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

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

328
    @rtype: objects.BlockDevStatus
329

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

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

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

346
    @rtype: objects.BlockDevStatus
347

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

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

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

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

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

    
370
        is_degraded = is_degraded or child_status.is_degraded
371

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

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

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

388
    Only supported for some device types.
389

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

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

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

406
    """
407
    raise NotImplementedError
408

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

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

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

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

    
431

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
535
      data.append(splitted_fields)
536

    
537
    return data
538

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

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

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

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

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

    
567
    return data
568

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

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

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

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

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

    
598
    return data
599

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

726
    """
727
    pass
728

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

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

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

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

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

748
    @rtype: objects.BlockDevStatus
749

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

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

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

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

769
    """
770
    pass
771

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

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

777
    """
778
    pass
779

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

783
    @returns: tuple (vg, lv)
784

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

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

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

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

    
806
    return (self._vg_name, snap_name)
807

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

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

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

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

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

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

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

    
853

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

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

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

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

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

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

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

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

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

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

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

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

    
961

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1056
    return retval
1057

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

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

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

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

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

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

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

    
1100
    return used_devs
1101

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

1105
    This sets our minor variable and our dev_path.
1106

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

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

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

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

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

1147
    This is not supported for drbd devices.
1148

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

    
1152

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

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

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

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

    
1169
  # timeout constants
1170
  _NET_RECONFIG_TIMEOUT = 60
1171

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1309
    cls._PARSE_SHOW = bnf
1310

    
1311
    return bnf
1312

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1465
    args = []
1466

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

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

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

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

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

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

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

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

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

    
1520
    return args
1521

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1646
    This is the low-level implementation.
1647

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

1655
    """
1656

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

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

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

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

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

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

    
1697
    return []
1698

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

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

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

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

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

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

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

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

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

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

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

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

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

1756

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

1761

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

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

1768
    @rtype: objects.BlockDevStatus
1769

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1862
      raise utils.RetryAgain()
1863

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

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

    
1878
      _ThrowError(msg, self.minor)
1879

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

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

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

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

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

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

    
1902
    status = self.GetProcStatus()
1903

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
2025
    else:
2026
      minor = None
2027

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2100
    """
2101
    self.Shutdown()
2102

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

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

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

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

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

    
2150

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

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

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

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

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

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

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

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

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

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

2187
    """
2188
    pass
2189

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

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

2195
    """
2196
    pass
2197

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

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

2203
    """
2204
    pass
2205

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

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

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

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

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

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

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

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

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

2252
    Check if this file already exists.
2253

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

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

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

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

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

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

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

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

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

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

    
2299

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

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

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

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

2313
    The unique_id is a path under /dev.
2314

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

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

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

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

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

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

2347
    This is a noop
2348

2349
    """
2350
    pass
2351

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

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

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

2361

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

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

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

    
2378
    return True
2379

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

2383
    """
2384
    pass
2385

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

2389
    """
2390
    pass
2391

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

2395
    """
2396
    pass
2397

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

2401
    """
2402
    pass
2403

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

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

    
2410

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

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

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

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

    
2427
    self.driver, self.rbd_name = unique_id
2428

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

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

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

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

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

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

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

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

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

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

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

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

2479
    """
2480
    pass
2481

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

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

2488
    """
2489
    self.attached = False
2490

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

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

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

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

    
2508
    return True
2509

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
2578
    field_sep = "\t"
2579

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

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

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

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

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

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

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

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

2614
    """
2615
    pass
2616

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

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

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

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

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

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

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

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

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

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

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

2661
    """
2662
    pass
2663

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

2667
    """
2668
    pass
2669

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

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

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

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

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

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

    
2703

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

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

    
2714

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

    
2719

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

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

    
2729

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

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

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

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

    
2750

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

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

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

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

    
2771

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

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

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