Statistics
| Branch: | Tag: | Revision:

root / lib / bdev.py @ f24d3d3b

History | View | Annotate | Download (109.3 kB)

1
#
2
#
3

    
4
# Copyright (C) 2006, 2007, 2010, 2011, 2012, 2013 Google Inc.
5
#
6
# This program is free software; you can redistribute it and/or modify
7
# it under the terms of the GNU General Public License as published by
8
# the Free Software Foundation; either version 2 of the License, or
9
# (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful, but
12
# WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14
# General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19
# 02110-1301, USA.
20

    
21

    
22
"""Block device abstraction"""
23

    
24
import re
25
import time
26
import errno
27
import shlex
28
import stat
29
import pyparsing as pyp
30
import os
31
import logging
32
import math
33

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

    
43

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

    
47

    
48
class RbdShowmappedJsonError(Exception):
49
  """`rbd showmmapped' JSON formatting error Exception class.
50

51
  """
52
  pass
53

    
54

    
55
def _IgnoreError(fn, *args, **kwargs):
56
  """Executes the given function, ignoring BlockDeviceErrors.
57

58
  This is used in order to simplify the execution of cleanup or
59
  rollback functions.
60

61
  @rtype: boolean
62
  @return: True when fn didn't raise an exception, False otherwise
63

64
  """
65
  try:
66
    fn(*args, **kwargs)
67
    return True
68
  except errors.BlockDeviceError, err:
69
    logging.warning("Caught BlockDeviceError but ignoring: %s", str(err))
70
    return False
71

    
72

    
73
def _ThrowError(msg, *args):
74
  """Log an error to the node daemon and the raise an exception.
75

76
  @type msg: string
77
  @param msg: the text of the exception
78
  @raise errors.BlockDeviceError
79

80
  """
81
  if args:
82
    msg = msg % args
83
  logging.error(msg)
84
  raise errors.BlockDeviceError(msg)
85

    
86

    
87
def _CheckResult(result):
88
  """Throws an error if the given result is a failed one.
89

90
  @param result: result from RunCmd
91

92
  """
93
  if result.failed:
94
    _ThrowError("Command: %s error: %s - %s", result.cmd, result.fail_reason,
95
                result.output)
96

    
97

    
98
def _CanReadDevice(path):
99
  """Check if we can read from the given device.
100

101
  This tries to read the first 128k of the device.
102

103
  """
104
  try:
105
    utils.ReadFile(path, size=_DEVICE_READ_SIZE)
106
    return True
107
  except EnvironmentError:
108
    logging.warning("Can't read from device %s", path, exc_info=True)
109
    return False
110

    
111

    
112
def _GetForbiddenFileStoragePaths():
113
  """Builds a list of path prefixes which shouldn't be used for file storage.
114

115
  @rtype: frozenset
116

117
  """
118
  paths = set([
119
    "/boot",
120
    "/dev",
121
    "/etc",
122
    "/home",
123
    "/proc",
124
    "/root",
125
    "/sys",
126
    ])
127

    
128
  for prefix in ["", "/usr", "/usr/local"]:
129
    paths.update(map(lambda s: "%s/%s" % (prefix, s),
130
                     ["bin", "lib", "lib32", "lib64", "sbin"]))
131

    
132
  return compat.UniqueFrozenset(map(os.path.normpath, paths))
133

    
134

    
135
def _ComputeWrongFileStoragePaths(paths,
136
                                  _forbidden=_GetForbiddenFileStoragePaths()):
137
  """Cross-checks a list of paths for prefixes considered bad.
138

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

141
  @type paths: list
142
  @param paths: List of paths to be checked
143
  @rtype: list
144
  @return: Sorted list of paths for which the user should be warned
145

146
  """
147
  def _Check(path):
148
    return (not os.path.isabs(path) or
149
            path in _forbidden or
150
            filter(lambda p: utils.IsBelowDir(p, path), _forbidden))
151

    
152
  return utils.NiceSort(filter(_Check, map(os.path.normpath, paths)))
153

    
154

    
155
def ComputeWrongFileStoragePaths(_filename=pathutils.FILE_STORAGE_PATHS_FILE):
156
  """Returns a list of file storage paths whose prefix is considered bad.
157

158
  See L{_ComputeWrongFileStoragePaths}.
159

160
  """
161
  return _ComputeWrongFileStoragePaths(_LoadAllowedFileStoragePaths(_filename))
162

    
163

    
164
def _CheckFileStoragePath(path, allowed):
165
  """Checks if a path is in a list of allowed paths for file storage.
166

167
  @type path: string
168
  @param path: Path to check
169
  @type allowed: list
170
  @param allowed: List of allowed paths
171
  @raise errors.FileStoragePathError: If the path is not allowed
172

173
  """
174
  if not os.path.isabs(path):
175
    raise errors.FileStoragePathError("File storage path must be absolute,"
176
                                      " got '%s'" % path)
177

    
178
  for i in allowed:
179
    if not os.path.isabs(i):
180
      logging.info("Ignoring relative path '%s' for file storage", i)
181
      continue
182

    
183
    if utils.IsBelowDir(i, path):
184
      break
185
  else:
186
    raise errors.FileStoragePathError("Path '%s' is not acceptable for file"
187
                                      " storage. A possible fix might be to add"
188
                                      " it to /etc/ganeti/file-storage-paths"
189
                                      " on all nodes." % path)
190

    
191

    
192
def _LoadAllowedFileStoragePaths(filename):
193
  """Loads file containing allowed file storage paths.
194

195
  @rtype: list
196
  @return: List of allowed paths (can be an empty list)
197

198
  """
199
  try:
200
    contents = utils.ReadFile(filename)
201
  except EnvironmentError:
202
    return []
203
  else:
204
    return utils.FilterEmptyLinesAndComments(contents)
205

    
206

    
207
def CheckFileStoragePath(path, _filename=pathutils.FILE_STORAGE_PATHS_FILE):
208
  """Checks if a path is allowed for file storage.
209

210
  @type path: string
211
  @param path: Path to check
212
  @raise errors.FileStoragePathError: If the path is not allowed
213

214
  """
215
  allowed = _LoadAllowedFileStoragePaths(_filename)
216

    
217
  if _ComputeWrongFileStoragePaths([path]):
218
    raise errors.FileStoragePathError("Path '%s' uses a forbidden prefix" %
219
                                      path)
220

    
221
  _CheckFileStoragePath(path, allowed)
222

    
223

    
224
class BlockDev(object):
225
  """Block device abstract class.
226

227
  A block device can be in the following states:
228
    - not existing on the system, and by `Create()` it goes into:
229
    - existing but not setup/not active, and by `Assemble()` goes into:
230
    - active read-write and by `Open()` it goes into
231
    - online (=used, or ready for use)
232

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

239
  The many different states of the device are due to the fact that we
240
  need to cover many device types:
241
    - logical volumes are created, lvchange -a y $lv, and used
242
    - drbd devices are attached to a local disk/remote peer and made primary
243

244
  A block device is identified by three items:
245
    - the /dev path of the device (dynamic)
246
    - a unique ID of the device (static)
247
    - it's major/minor pair (dynamic)
248

249
  Not all devices implement both the first two as distinct items. LVM
250
  logical volumes have their unique ID (the pair volume group, logical
251
  volume name) in a 1-to-1 relation to the dev path. For DRBD devices,
252
  the /dev path is again dynamic and the unique id is the pair (host1,
253
  dev1), (host2, dev2).
254

255
  You can get to a device in two ways:
256
    - creating the (real) device, which returns you
257
      an attached instance (lvcreate)
258
    - attaching of a python instance to an existing (real) device
259

260
  The second point, the attachment to a device, is different
261
  depending on whether the device is assembled or not. At init() time,
262
  we search for a device with the same unique_id as us. If found,
263
  good. It also means that the device is already assembled. If not,
264
  after assembly we'll have our correct major/minor.
265

266
  """
267
  def __init__(self, unique_id, children, size, params):
268
    self._children = children
269
    self.dev_path = None
270
    self.unique_id = unique_id
271
    self.major = None
272
    self.minor = None
273
    self.attached = False
274
    self.size = size
275
    self.params = params
276

    
277
  def Assemble(self):
278
    """Assemble the device from its components.
279

280
    Implementations of this method by child classes must ensure that:
281
      - after the device has been assembled, it knows its major/minor
282
        numbers; this allows other devices (usually parents) to probe
283
        correctly for their children
284
      - calling this method on an existing, in-use device is safe
285
      - if the device is already configured (and in an OK state),
286
        this method is idempotent
287

288
    """
289
    pass
290

    
291
  def Attach(self):
292
    """Find a device which matches our config and attach to it.
293

294
    """
295
    raise NotImplementedError
296

    
297
  def Close(self):
298
    """Notifies that the device will no longer be used for I/O.
299

300
    """
301
    raise NotImplementedError
302

    
303
  @classmethod
304
  def Create(cls, unique_id, children, size, params, excl_stor):
305
    """Create the device.
306

307
    If the device cannot be created, it will return None
308
    instead. Error messages go to the logging system.
309

310
    Note that for some devices, the unique_id is used, and for other,
311
    the children. The idea is that these two, taken together, are
312
    enough for both creation and assembly (later).
313

314
    """
315
    raise NotImplementedError
316

    
317
  def Remove(self):
318
    """Remove this device.
319

320
    This makes sense only for some of the device types: LV and file
321
    storage. Also note that if the device can't attach, the removal
322
    can't be completed.
323

324
    """
325
    raise NotImplementedError
326

    
327
  def Rename(self, new_id):
328
    """Rename this device.
329

330
    This may or may not make sense for a given device type.
331

332
    """
333
    raise NotImplementedError
334

    
335
  def Open(self, force=False):
336
    """Make the device ready for use.
337

338
    This makes the device ready for I/O. For now, just the DRBD
339
    devices need this.
340

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

344
    """
345
    raise NotImplementedError
346

    
347
  def Shutdown(self):
348
    """Shut down the device, freeing its children.
349

350
    This undoes the `Assemble()` work, except for the child
351
    assembling; as such, the children on the device are still
352
    assembled after this call.
353

354
    """
355
    raise NotImplementedError
356

    
357
  def SetSyncParams(self, params):
358
    """Adjust the synchronization parameters of the mirror.
359

360
    In case this is not a mirroring device, this is no-op.
361

362
    @param params: dictionary of LD level disk parameters related to the
363
    synchronization.
364
    @rtype: list
365
    @return: a list of error messages, emitted both by the current node and by
366
    children. An empty list means no errors.
367

368
    """
369
    result = []
370
    if self._children:
371
      for child in self._children:
372
        result.extend(child.SetSyncParams(params))
373
    return result
374

    
375
  def PauseResumeSync(self, pause):
376
    """Pause/Resume the sync of the mirror.
377

378
    In case this is not a mirroring device, this is no-op.
379

380
    @param pause: Whether to pause or resume
381

382
    """
383
    result = True
384
    if self._children:
385
      for child in self._children:
386
        result = result and child.PauseResumeSync(pause)
387
    return result
388

    
389
  def GetSyncStatus(self):
390
    """Returns the sync status of the device.
391

392
    If this device is a mirroring device, this function returns the
393
    status of the mirror.
394

395
    If sync_percent is None, it means the device is not syncing.
396

397
    If estimated_time is None, it means we can't estimate
398
    the time needed, otherwise it's the time left in seconds.
399

400
    If is_degraded is True, it means the device is missing
401
    redundancy. This is usually a sign that something went wrong in
402
    the device setup, if sync_percent is None.
403

404
    The ldisk parameter represents the degradation of the local
405
    data. This is only valid for some devices, the rest will always
406
    return False (not degraded).
407

408
    @rtype: objects.BlockDevStatus
409

410
    """
411
    return objects.BlockDevStatus(dev_path=self.dev_path,
412
                                  major=self.major,
413
                                  minor=self.minor,
414
                                  sync_percent=None,
415
                                  estimated_time=None,
416
                                  is_degraded=False,
417
                                  ldisk_status=constants.LDS_OKAY)
418

    
419
  def CombinedSyncStatus(self):
420
    """Calculate the mirror status recursively for our children.
421

422
    The return value is the same as for `GetSyncStatus()` except the
423
    minimum percent and maximum time are calculated across our
424
    children.
425

426
    @rtype: objects.BlockDevStatus
427

428
    """
429
    status = self.GetSyncStatus()
430

    
431
    min_percent = status.sync_percent
432
    max_time = status.estimated_time
433
    is_degraded = status.is_degraded
434
    ldisk_status = status.ldisk_status
435

    
436
    if self._children:
437
      for child in self._children:
438
        child_status = child.GetSyncStatus()
439

    
440
        if min_percent is None:
441
          min_percent = child_status.sync_percent
442
        elif child_status.sync_percent is not None:
443
          min_percent = min(min_percent, child_status.sync_percent)
444

    
445
        if max_time is None:
446
          max_time = child_status.estimated_time
447
        elif child_status.estimated_time is not None:
448
          max_time = max(max_time, child_status.estimated_time)
449

    
450
        is_degraded = is_degraded or child_status.is_degraded
451

    
452
        if ldisk_status is None:
453
          ldisk_status = child_status.ldisk_status
454
        elif child_status.ldisk_status is not None:
455
          ldisk_status = max(ldisk_status, child_status.ldisk_status)
456

    
457
    return objects.BlockDevStatus(dev_path=self.dev_path,
458
                                  major=self.major,
459
                                  minor=self.minor,
460
                                  sync_percent=min_percent,
461
                                  estimated_time=max_time,
462
                                  is_degraded=is_degraded,
463
                                  ldisk_status=ldisk_status)
464

    
465
  def SetInfo(self, text):
466
    """Update metadata with info text.
467

468
    Only supported for some device types.
469

470
    """
471
    for child in self._children:
472
      child.SetInfo(text)
473

    
474
  def Grow(self, amount, dryrun, backingstore):
475
    """Grow the block device.
476

477
    @type amount: integer
478
    @param amount: the amount (in mebibytes) to grow with
479
    @type dryrun: boolean
480
    @param dryrun: whether to execute the operation in simulation mode
481
        only, without actually increasing the size
482
    @param backingstore: whether to execute the operation on backing storage
483
        only, or on "logical" storage only; e.g. DRBD is logical storage,
484
        whereas LVM, file, RBD are backing storage
485

486
    """
487
    raise NotImplementedError
488

    
489
  def GetActualSize(self):
490
    """Return the actual disk size.
491

492
    @note: the device needs to be active when this is called
493

494
    """
495
    assert self.attached, "BlockDevice not attached in GetActualSize()"
496
    result = utils.RunCmd(["blockdev", "--getsize64", self.dev_path])
497
    if result.failed:
498
      _ThrowError("blockdev failed (%s): %s",
499
                  result.fail_reason, result.output)
500
    try:
501
      sz = int(result.output.strip())
502
    except (ValueError, TypeError), err:
503
      _ThrowError("Failed to parse blockdev output: %s", str(err))
504
    return sz
505

    
506
  def __repr__(self):
507
    return ("<%s: unique_id: %s, children: %s, %s:%s, %s>" %
508
            (self.__class__, self.unique_id, self._children,
509
             self.major, self.minor, self.dev_path))
510

    
511

    
512
class LogicalVolume(BlockDev):
513
  """Logical Volume block device.
514

515
  """
516
  _VALID_NAME_RE = re.compile("^[a-zA-Z0-9+_.-]*$")
517
  _INVALID_NAMES = compat.UniqueFrozenset([".", "..", "snapshot", "pvmove"])
518
  _INVALID_SUBSTRINGS = compat.UniqueFrozenset(["_mlog", "_mimage"])
519

    
520
  def __init__(self, unique_id, children, size, params):
521
    """Attaches to a LV device.
522

523
    The unique_id is a tuple (vg_name, lv_name)
524

525
    """
526
    super(LogicalVolume, self).__init__(unique_id, children, size, params)
527
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
528
      raise ValueError("Invalid configuration data %s" % str(unique_id))
529
    self._vg_name, self._lv_name = unique_id
530
    self._ValidateName(self._vg_name)
531
    self._ValidateName(self._lv_name)
532
    self.dev_path = utils.PathJoin("/dev", self._vg_name, self._lv_name)
533
    self._degraded = True
534
    self.major = self.minor = self.pe_size = self.stripe_count = None
535
    self.Attach()
536

    
537
  @staticmethod
538
  def _GetStdPvSize(pvs_info):
539
    """Return the the standard PV size (used with exclusive storage).
540

541
    @param pvs_info: list of objects.LvmPvInfo, cannot be empty
542
    @rtype: float
543
    @return: size in MiB
544

545
    """
546
    assert len(pvs_info) > 0
547
    smallest = min([pv.size for pv in pvs_info])
548
    return smallest / (1 + constants.PART_MARGIN + constants.PART_RESERVED)
549

    
550
  @staticmethod
551
  def _ComputeNumPvs(size, pvs_info):
552
    """Compute the number of PVs needed for an LV (with exclusive storage).
553

554
    @type size: float
555
    @param size: LV size in MiB
556
    @param pvs_info: list of objects.LvmPvInfo, cannot be empty
557
    @rtype: integer
558
    @return: number of PVs needed
559
    """
560
    assert len(pvs_info) > 0
561
    pv_size = float(LogicalVolume._GetStdPvSize(pvs_info))
562
    return int(math.ceil(float(size) / pv_size))
563

    
564
  @staticmethod
565
  def _GetEmptyPvNames(pvs_info, max_pvs=None):
566
    """Return a list of empty PVs, by name.
567

568
    """
569
    empty_pvs = filter(objects.LvmPvInfo.IsEmpty, pvs_info)
570
    if max_pvs is not None:
571
      empty_pvs = empty_pvs[:max_pvs]
572
    return map((lambda pv: pv.name), empty_pvs)
573

    
574
  @classmethod
575
  def Create(cls, unique_id, children, size, params, excl_stor):
576
    """Create a new logical volume.
577

578
    """
579
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
580
      raise errors.ProgrammerError("Invalid configuration data %s" %
581
                                   str(unique_id))
582
    vg_name, lv_name = unique_id
583
    cls._ValidateName(vg_name)
584
    cls._ValidateName(lv_name)
585
    pvs_info = cls.GetPVInfo([vg_name])
586
    if not pvs_info:
587
      if excl_stor:
588
        msg = "No (empty) PVs found"
589
      else:
590
        msg = "Can't compute PV info for vg %s" % vg_name
591
      _ThrowError(msg)
592
    pvs_info.sort(key=(lambda pv: pv.free), reverse=True)
593

    
594
    pvlist = [pv.name for pv in pvs_info]
595
    if compat.any(":" in v for v in pvlist):
596
      _ThrowError("Some of your PVs have the invalid character ':' in their"
597
                  " name, this is not supported - please filter them out"
598
                  " in lvm.conf using either 'filter' or 'preferred_names'")
599

    
600
    current_pvs = len(pvlist)
601
    desired_stripes = params[constants.LDP_STRIPES]
602
    stripes = min(current_pvs, desired_stripes)
603

    
604
    if excl_stor:
605
      (err_msgs, _) = utils.LvmExclusiveCheckNodePvs(pvs_info)
606
      if err_msgs:
607
        for m in err_msgs:
608
          logging.warning(m)
609
      req_pvs = cls._ComputeNumPvs(size, pvs_info)
610
      pvlist = cls._GetEmptyPvNames(pvs_info, req_pvs)
611
      current_pvs = len(pvlist)
612
      if current_pvs < req_pvs:
613
        _ThrowError("Not enough empty PVs to create a disk of %d MB:"
614
                    " %d available, %d needed", size, current_pvs, req_pvs)
615
      assert current_pvs == len(pvlist)
616
      if stripes > current_pvs:
617
        # No warning issued for this, as it's no surprise
618
        stripes = current_pvs
619

    
620
    else:
621
      if stripes < desired_stripes:
622
        logging.warning("Could not use %d stripes for VG %s, as only %d PVs are"
623
                        " available.", desired_stripes, vg_name, current_pvs)
624
      free_size = sum([pv.free for pv in pvs_info])
625
      # The size constraint should have been checked from the master before
626
      # calling the create function.
627
      if free_size < size:
628
        _ThrowError("Not enough free space: required %s,"
629
                    " available %s", size, free_size)
630

    
631
    # If the free space is not well distributed, we won't be able to
632
    # create an optimally-striped volume; in that case, we want to try
633
    # with N, N-1, ..., 2, and finally 1 (non-stripped) number of
634
    # stripes
635
    cmd = ["lvcreate", "-L%dm" % size, "-n%s" % lv_name]
636
    for stripes_arg in range(stripes, 0, -1):
637
      result = utils.RunCmd(cmd + ["-i%d" % stripes_arg] + [vg_name] + pvlist)
638
      if not result.failed:
639
        break
640
    if result.failed:
641
      _ThrowError("LV create failed (%s): %s",
642
                  result.fail_reason, result.output)
643
    return LogicalVolume(unique_id, children, size, params)
644

    
645
  @staticmethod
646
  def _GetVolumeInfo(lvm_cmd, fields):
647
    """Returns LVM Volumen infos using lvm_cmd
648

649
    @param lvm_cmd: Should be one of "pvs", "vgs" or "lvs"
650
    @param fields: Fields to return
651
    @return: A list of dicts each with the parsed fields
652

653
    """
654
    if not fields:
655
      raise errors.ProgrammerError("No fields specified")
656

    
657
    sep = "|"
658
    cmd = [lvm_cmd, "--noheadings", "--nosuffix", "--units=m", "--unbuffered",
659
           "--separator=%s" % sep, "-o%s" % ",".join(fields)]
660

    
661
    result = utils.RunCmd(cmd)
662
    if result.failed:
663
      raise errors.CommandError("Can't get the volume information: %s - %s" %
664
                                (result.fail_reason, result.output))
665

    
666
    data = []
667
    for line in result.stdout.splitlines():
668
      splitted_fields = line.strip().split(sep)
669

    
670
      if len(fields) != len(splitted_fields):
671
        raise errors.CommandError("Can't parse %s output: line '%s'" %
672
                                  (lvm_cmd, line))
673

    
674
      data.append(splitted_fields)
675

    
676
    return data
677

    
678
  @classmethod
679
  def GetPVInfo(cls, vg_names, filter_allocatable=True, include_lvs=False):
680
    """Get the free space info for PVs in a volume group.
681

682
    @param vg_names: list of volume group names, if empty all will be returned
683
    @param filter_allocatable: whether to skip over unallocatable PVs
684
    @param include_lvs: whether to include a list of LVs hosted on each PV
685

686
    @rtype: list
687
    @return: list of objects.LvmPvInfo objects
688

689
    """
690
    # We request "lv_name" field only if we care about LVs, so we don't get
691
    # a long list of entries with many duplicates unless we really have to.
692
    # The duplicate "pv_name" field will be ignored.
693
    if include_lvs:
694
      lvfield = "lv_name"
695
    else:
696
      lvfield = "pv_name"
697
    try:
698
      info = cls._GetVolumeInfo("pvs", ["pv_name", "vg_name", "pv_free",
699
                                        "pv_attr", "pv_size", lvfield])
700
    except errors.GenericError, err:
701
      logging.error("Can't get PV information: %s", err)
702
      return None
703

    
704
    # When asked for LVs, "pvs" may return multiple entries for the same PV-LV
705
    # pair. We sort entries by PV name and then LV name, so it's easy to weed
706
    # out duplicates.
707
    if include_lvs:
708
      info.sort(key=(lambda i: (i[0], i[5])))
709
    data = []
710
    lastpvi = None
711
    for (pv_name, vg_name, pv_free, pv_attr, pv_size, lv_name) in info:
712
      # (possibly) skip over pvs which are not allocatable
713
      if filter_allocatable and pv_attr[0] != "a":
714
        continue
715
      # (possibly) skip over pvs which are not in the right volume group(s)
716
      if vg_names and vg_name not in vg_names:
717
        continue
718
      # Beware of duplicates (check before inserting)
719
      if lastpvi and lastpvi.name == pv_name:
720
        if include_lvs and lv_name:
721
          if not lastpvi.lv_list or lastpvi.lv_list[-1] != lv_name:
722
            lastpvi.lv_list.append(lv_name)
723
      else:
724
        if include_lvs and lv_name:
725
          lvl = [lv_name]
726
        else:
727
          lvl = []
728
        lastpvi = objects.LvmPvInfo(name=pv_name, vg_name=vg_name,
729
                                    size=float(pv_size), free=float(pv_free),
730
                                    attributes=pv_attr, lv_list=lvl)
731
        data.append(lastpvi)
732

    
733
    return data
734

    
735
  @classmethod
736
  def _GetExclusiveStorageVgFree(cls, vg_name):
737
    """Return the free disk space in the given VG, in exclusive storage mode.
738

739
    @type vg_name: string
740
    @param vg_name: VG name
741
    @rtype: float
742
    @return: free space in MiB
743
    """
744
    pvs_info = cls.GetPVInfo([vg_name])
745
    if not pvs_info:
746
      return 0.0
747
    pv_size = cls._GetStdPvSize(pvs_info)
748
    num_pvs = len(cls._GetEmptyPvNames(pvs_info))
749
    return pv_size * num_pvs
750

    
751
  @classmethod
752
  def GetVGInfo(cls, vg_names, excl_stor, filter_readonly=True):
753
    """Get the free space info for specific VGs.
754

755
    @param vg_names: list of volume group names, if empty all will be returned
756
    @param excl_stor: whether exclusive_storage is enabled
757
    @param filter_readonly: whether to skip over readonly VGs
758

759
    @rtype: list
760
    @return: list of tuples (free_space, total_size, name) with free_space in
761
             MiB
762

763
    """
764
    try:
765
      info = cls._GetVolumeInfo("vgs", ["vg_name", "vg_free", "vg_attr",
766
                                        "vg_size"])
767
    except errors.GenericError, err:
768
      logging.error("Can't get VG information: %s", err)
769
      return None
770

    
771
    data = []
772
    for vg_name, vg_free, vg_attr, vg_size in info:
773
      # (possibly) skip over vgs which are not writable
774
      if filter_readonly and vg_attr[0] == "r":
775
        continue
776
      # (possibly) skip over vgs which are not in the right volume group(s)
777
      if vg_names and vg_name not in vg_names:
778
        continue
779
      # Exclusive storage needs a different concept of free space
780
      if excl_stor:
781
        es_free = cls._GetExclusiveStorageVgFree(vg_name)
782
        assert es_free <= vg_free
783
        vg_free = es_free
784
      data.append((float(vg_free), float(vg_size), vg_name))
785

    
786
    return data
787

    
788
  @classmethod
789
  def _ValidateName(cls, name):
790
    """Validates that a given name is valid as VG or LV name.
791

792
    The list of valid characters and restricted names is taken out of
793
    the lvm(8) manpage, with the simplification that we enforce both
794
    VG and LV restrictions on the names.
795

796
    """
797
    if (not cls._VALID_NAME_RE.match(name) or
798
        name in cls._INVALID_NAMES or
799
        compat.any(substring in name for substring in cls._INVALID_SUBSTRINGS)):
800
      _ThrowError("Invalid LVM name '%s'", name)
801

    
802
  def Remove(self):
803
    """Remove this logical volume.
804

805
    """
806
    if not self.minor and not self.Attach():
807
      # the LV does not exist
808
      return
809
    result = utils.RunCmd(["lvremove", "-f", "%s/%s" %
810
                           (self._vg_name, self._lv_name)])
811
    if result.failed:
812
      _ThrowError("Can't lvremove: %s - %s", result.fail_reason, result.output)
813

    
814
  def Rename(self, new_id):
815
    """Rename this logical volume.
816

817
    """
818
    if not isinstance(new_id, (tuple, list)) or len(new_id) != 2:
819
      raise errors.ProgrammerError("Invalid new logical id '%s'" % new_id)
820
    new_vg, new_name = new_id
821
    if new_vg != self._vg_name:
822
      raise errors.ProgrammerError("Can't move a logical volume across"
823
                                   " volume groups (from %s to to %s)" %
824
                                   (self._vg_name, new_vg))
825
    result = utils.RunCmd(["lvrename", new_vg, self._lv_name, new_name])
826
    if result.failed:
827
      _ThrowError("Failed to rename the logical volume: %s", result.output)
828
    self._lv_name = new_name
829
    self.dev_path = utils.PathJoin("/dev", self._vg_name, self._lv_name)
830

    
831
  def Attach(self):
832
    """Attach to an existing LV.
833

834
    This method will try to see if an existing and active LV exists
835
    which matches our name. If so, its major/minor will be
836
    recorded.
837

838
    """
839
    self.attached = False
840
    result = utils.RunCmd(["lvs", "--noheadings", "--separator=,",
841
                           "--units=k", "--nosuffix",
842
                           "-olv_attr,lv_kernel_major,lv_kernel_minor,"
843
                           "vg_extent_size,stripes", self.dev_path])
844
    if result.failed:
845
      logging.error("Can't find LV %s: %s, %s",
846
                    self.dev_path, result.fail_reason, result.output)
847
      return False
848
    # the output can (and will) have multiple lines for multi-segment
849
    # LVs, as the 'stripes' parameter is a segment one, so we take
850
    # only the last entry, which is the one we're interested in; note
851
    # that with LVM2 anyway the 'stripes' value must be constant
852
    # across segments, so this is a no-op actually
853
    out = result.stdout.splitlines()
854
    if not out: # totally empty result? splitlines() returns at least
855
                # one line for any non-empty string
856
      logging.error("Can't parse LVS output, no lines? Got '%s'", str(out))
857
      return False
858
    out = out[-1].strip().rstrip(",")
859
    out = out.split(",")
860
    if len(out) != 5:
861
      logging.error("Can't parse LVS output, len(%s) != 5", str(out))
862
      return False
863

    
864
    status, major, minor, pe_size, stripes = out
865
    if len(status) < 6:
866
      logging.error("lvs lv_attr is not at least 6 characters (%s)", status)
867
      return False
868

    
869
    try:
870
      major = int(major)
871
      minor = int(minor)
872
    except (TypeError, ValueError), err:
873
      logging.error("lvs major/minor cannot be parsed: %s", str(err))
874

    
875
    try:
876
      pe_size = int(float(pe_size))
877
    except (TypeError, ValueError), err:
878
      logging.error("Can't parse vg extent size: %s", err)
879
      return False
880

    
881
    try:
882
      stripes = int(stripes)
883
    except (TypeError, ValueError), err:
884
      logging.error("Can't parse the number of stripes: %s", err)
885
      return False
886

    
887
    self.major = major
888
    self.minor = minor
889
    self.pe_size = pe_size
890
    self.stripe_count = stripes
891
    self._degraded = status[0] == "v" # virtual volume, i.e. doesn't backing
892
                                      # storage
893
    self.attached = True
894
    return True
895

    
896
  def Assemble(self):
897
    """Assemble the device.
898

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

903
    """
904
    result = utils.RunCmd(["lvchange", "-ay", self.dev_path])
905
    if result.failed:
906
      _ThrowError("Can't activate lv %s: %s", self.dev_path, result.output)
907

    
908
  def Shutdown(self):
909
    """Shutdown the device.
910

911
    This is a no-op for the LV device type, as we don't deactivate the
912
    volumes on shutdown.
913

914
    """
915
    pass
916

    
917
  def GetSyncStatus(self):
918
    """Returns the sync status of the device.
919

920
    If this device is a mirroring device, this function returns the
921
    status of the mirror.
922

923
    For logical volumes, sync_percent and estimated_time are always
924
    None (no recovery in progress, as we don't handle the mirrored LV
925
    case). The is_degraded parameter is the inverse of the ldisk
926
    parameter.
927

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

934
    The status was already read in Attach, so we just return it.
935

936
    @rtype: objects.BlockDevStatus
937

938
    """
939
    if self._degraded:
940
      ldisk_status = constants.LDS_FAULTY
941
    else:
942
      ldisk_status = constants.LDS_OKAY
943

    
944
    return objects.BlockDevStatus(dev_path=self.dev_path,
945
                                  major=self.major,
946
                                  minor=self.minor,
947
                                  sync_percent=None,
948
                                  estimated_time=None,
949
                                  is_degraded=self._degraded,
950
                                  ldisk_status=ldisk_status)
951

    
952
  def Open(self, force=False):
953
    """Make the device ready for I/O.
954

955
    This is a no-op for the LV device type.
956

957
    """
958
    pass
959

    
960
  def Close(self):
961
    """Notifies that the device will no longer be used for I/O.
962

963
    This is a no-op for the LV device type.
964

965
    """
966
    pass
967

    
968
  def Snapshot(self, size):
969
    """Create a snapshot copy of an lvm block device.
970

971
    @returns: tuple (vg, lv)
972

973
    """
974
    snap_name = self._lv_name + ".snap"
975

    
976
    # remove existing snapshot if found
977
    snap = LogicalVolume((self._vg_name, snap_name), None, size, self.params)
978
    _IgnoreError(snap.Remove)
979

    
980
    vg_info = self.GetVGInfo([self._vg_name], False)
981
    if not vg_info:
982
      _ThrowError("Can't compute VG info for vg %s", self._vg_name)
983
    free_size, _, _ = vg_info[0]
984
    if free_size < size:
985
      _ThrowError("Not enough free space: required %s,"
986
                  " available %s", size, free_size)
987

    
988
    _CheckResult(utils.RunCmd(["lvcreate", "-L%dm" % size, "-s",
989
                               "-n%s" % snap_name, self.dev_path]))
990

    
991
    return (self._vg_name, snap_name)
992

    
993
  def _RemoveOldInfo(self):
994
    """Try to remove old tags from the lv.
995

996
    """
997
    result = utils.RunCmd(["lvs", "-o", "tags", "--noheadings", "--nosuffix",
998
                           self.dev_path])
999
    _CheckResult(result)
1000

    
1001
    raw_tags = result.stdout.strip()
1002
    if raw_tags:
1003
      for tag in raw_tags.split(","):
1004
        _CheckResult(utils.RunCmd(["lvchange", "--deltag",
1005
                                   tag.strip(), self.dev_path]))
1006

    
1007
  def SetInfo(self, text):
1008
    """Update metadata with info text.
1009

1010
    """
1011
    BlockDev.SetInfo(self, text)
1012

    
1013
    self._RemoveOldInfo()
1014

    
1015
    # Replace invalid characters
1016
    text = re.sub("^[^A-Za-z0-9_+.]", "_", text)
1017
    text = re.sub("[^-A-Za-z0-9_+.]", "_", text)
1018

    
1019
    # Only up to 128 characters are allowed
1020
    text = text[:128]
1021

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

    
1024
  def Grow(self, amount, dryrun, backingstore):
1025
    """Grow the logical volume.
1026

1027
    """
1028
    if not backingstore:
1029
      return
1030
    if self.pe_size is None or self.stripe_count is None:
1031
      if not self.Attach():
1032
        _ThrowError("Can't attach to LV during Grow()")
1033
    full_stripe_size = self.pe_size * self.stripe_count
1034
    # pe_size is in KB
1035
    amount *= 1024
1036
    rest = amount % full_stripe_size
1037
    if rest != 0:
1038
      amount += full_stripe_size - rest
1039
    cmd = ["lvextend", "-L", "+%dk" % amount]
1040
    if dryrun:
1041
      cmd.append("--test")
1042
    # we try multiple algorithms since the 'best' ones might not have
1043
    # space available in the right place, but later ones might (since
1044
    # they have less constraints); also note that only recent LVM
1045
    # supports 'cling'
1046
    for alloc_policy in "contiguous", "cling", "normal":
1047
      result = utils.RunCmd(cmd + ["--alloc", alloc_policy, self.dev_path])
1048
      if not result.failed:
1049
        return
1050
    _ThrowError("Can't grow LV %s: %s", self.dev_path, result.output)
1051

    
1052

    
1053
class DRBD8Status(object):
1054
  """A DRBD status representation class.
1055

1056
  Note that this doesn't support unconfigured devices (cs:Unconfigured).
1057

1058
  """
1059
  UNCONF_RE = re.compile(r"\s*[0-9]+:\s*cs:Unconfigured$")
1060
  LINE_RE = re.compile(r"\s*[0-9]+:\s*cs:(\S+)\s+(?:st|ro):([^/]+)/(\S+)"
1061
                       "\s+ds:([^/]+)/(\S+)\s+.*$")
1062
  SYNC_RE = re.compile(r"^.*\ssync'ed:\s*([0-9.]+)%.*"
1063
                       # Due to a bug in drbd in the kernel, introduced in
1064
                       # commit 4b0715f096 (still unfixed as of 2011-08-22)
1065
                       "(?:\s|M)"
1066
                       "finish: ([0-9]+):([0-9]+):([0-9]+)\s.*$")
1067

    
1068
  CS_UNCONFIGURED = "Unconfigured"
1069
  CS_STANDALONE = "StandAlone"
1070
  CS_WFCONNECTION = "WFConnection"
1071
  CS_WFREPORTPARAMS = "WFReportParams"
1072
  CS_CONNECTED = "Connected"
1073
  CS_STARTINGSYNCS = "StartingSyncS"
1074
  CS_STARTINGSYNCT = "StartingSyncT"
1075
  CS_WFBITMAPS = "WFBitMapS"
1076
  CS_WFBITMAPT = "WFBitMapT"
1077
  CS_WFSYNCUUID = "WFSyncUUID"
1078
  CS_SYNCSOURCE = "SyncSource"
1079
  CS_SYNCTARGET = "SyncTarget"
1080
  CS_PAUSEDSYNCS = "PausedSyncS"
1081
  CS_PAUSEDSYNCT = "PausedSyncT"
1082
  CSET_SYNC = compat.UniqueFrozenset([
1083
    CS_WFREPORTPARAMS,
1084
    CS_STARTINGSYNCS,
1085
    CS_STARTINGSYNCT,
1086
    CS_WFBITMAPS,
1087
    CS_WFBITMAPT,
1088
    CS_WFSYNCUUID,
1089
    CS_SYNCSOURCE,
1090
    CS_SYNCTARGET,
1091
    CS_PAUSEDSYNCS,
1092
    CS_PAUSEDSYNCT,
1093
    ])
1094

    
1095
  DS_DISKLESS = "Diskless"
1096
  DS_ATTACHING = "Attaching" # transient state
1097
  DS_FAILED = "Failed" # transient state, next: diskless
1098
  DS_NEGOTIATING = "Negotiating" # transient state
1099
  DS_INCONSISTENT = "Inconsistent" # while syncing or after creation
1100
  DS_OUTDATED = "Outdated"
1101
  DS_DUNKNOWN = "DUnknown" # shown for peer disk when not connected
1102
  DS_CONSISTENT = "Consistent"
1103
  DS_UPTODATE = "UpToDate" # normal state
1104

    
1105
  RO_PRIMARY = "Primary"
1106
  RO_SECONDARY = "Secondary"
1107
  RO_UNKNOWN = "Unknown"
1108

    
1109
  def __init__(self, procline):
1110
    u = self.UNCONF_RE.match(procline)
1111
    if u:
1112
      self.cstatus = self.CS_UNCONFIGURED
1113
      self.lrole = self.rrole = self.ldisk = self.rdisk = None
1114
    else:
1115
      m = self.LINE_RE.match(procline)
1116
      if not m:
1117
        raise errors.BlockDeviceError("Can't parse input data '%s'" % procline)
1118
      self.cstatus = m.group(1)
1119
      self.lrole = m.group(2)
1120
      self.rrole = m.group(3)
1121
      self.ldisk = m.group(4)
1122
      self.rdisk = m.group(5)
1123

    
1124
    # end reading of data from the LINE_RE or UNCONF_RE
1125

    
1126
    self.is_standalone = self.cstatus == self.CS_STANDALONE
1127
    self.is_wfconn = self.cstatus == self.CS_WFCONNECTION
1128
    self.is_connected = self.cstatus == self.CS_CONNECTED
1129
    self.is_primary = self.lrole == self.RO_PRIMARY
1130
    self.is_secondary = self.lrole == self.RO_SECONDARY
1131
    self.peer_primary = self.rrole == self.RO_PRIMARY
1132
    self.peer_secondary = self.rrole == self.RO_SECONDARY
1133
    self.both_primary = self.is_primary and self.peer_primary
1134
    self.both_secondary = self.is_secondary and self.peer_secondary
1135

    
1136
    self.is_diskless = self.ldisk == self.DS_DISKLESS
1137
    self.is_disk_uptodate = self.ldisk == self.DS_UPTODATE
1138

    
1139
    self.is_in_resync = self.cstatus in self.CSET_SYNC
1140
    self.is_in_use = self.cstatus != self.CS_UNCONFIGURED
1141

    
1142
    m = self.SYNC_RE.match(procline)
1143
    if m:
1144
      self.sync_percent = float(m.group(1))
1145
      hours = int(m.group(2))
1146
      minutes = int(m.group(3))
1147
      seconds = int(m.group(4))
1148
      self.est_time = hours * 3600 + minutes * 60 + seconds
1149
    else:
1150
      # we have (in this if branch) no percent information, but if
1151
      # we're resyncing we need to 'fake' a sync percent information,
1152
      # as this is how cmdlib determines if it makes sense to wait for
1153
      # resyncing or not
1154
      if self.is_in_resync:
1155
        self.sync_percent = 0
1156
      else:
1157
        self.sync_percent = None
1158
      self.est_time = None
1159

    
1160

    
1161
class BaseDRBD(BlockDev): # pylint: disable=W0223
1162
  """Base DRBD class.
1163

1164
  This class contains a few bits of common functionality between the
1165
  0.7 and 8.x versions of DRBD.
1166

1167
  """
1168
  _VERSION_RE = re.compile(r"^version: (\d+)\.(\d+)\.(\d+)(?:\.\d+)?"
1169
                           r" \(api:(\d+)/proto:(\d+)(?:-(\d+))?\)")
1170
  _VALID_LINE_RE = re.compile("^ *([0-9]+): cs:([^ ]+).*$")
1171
  _UNUSED_LINE_RE = re.compile("^ *([0-9]+): cs:Unconfigured$")
1172

    
1173
  _DRBD_MAJOR = 147
1174
  _ST_UNCONFIGURED = "Unconfigured"
1175
  _ST_WFCONNECTION = "WFConnection"
1176
  _ST_CONNECTED = "Connected"
1177

    
1178
  _STATUS_FILE = constants.DRBD_STATUS_FILE
1179
  _USERMODE_HELPER_FILE = "/sys/module/drbd/parameters/usermode_helper"
1180

    
1181
  @staticmethod
1182
  def _GetProcData(filename=_STATUS_FILE):
1183
    """Return data from /proc/drbd.
1184

1185
    """
1186
    try:
1187
      data = utils.ReadFile(filename).splitlines()
1188
    except EnvironmentError, err:
1189
      if err.errno == errno.ENOENT:
1190
        _ThrowError("The file %s cannot be opened, check if the module"
1191
                    " is loaded (%s)", filename, str(err))
1192
      else:
1193
        _ThrowError("Can't read the DRBD proc file %s: %s", filename, str(err))
1194
    if not data:
1195
      _ThrowError("Can't read any data from %s", filename)
1196
    return data
1197

    
1198
  @classmethod
1199
  def _MassageProcData(cls, data):
1200
    """Transform the output of _GetProdData into a nicer form.
1201

1202
    @return: a dictionary of minor: joined lines from /proc/drbd
1203
        for that minor
1204

1205
    """
1206
    results = {}
1207
    old_minor = old_line = None
1208
    for line in data:
1209
      if not line: # completely empty lines, as can be returned by drbd8.0+
1210
        continue
1211
      lresult = cls._VALID_LINE_RE.match(line)
1212
      if lresult is not None:
1213
        if old_minor is not None:
1214
          results[old_minor] = old_line
1215
        old_minor = int(lresult.group(1))
1216
        old_line = line
1217
      else:
1218
        if old_minor is not None:
1219
          old_line += " " + line.strip()
1220
    # add last line
1221
    if old_minor is not None:
1222
      results[old_minor] = old_line
1223
    return results
1224

    
1225
  @classmethod
1226
  def _GetVersion(cls, proc_data):
1227
    """Return the DRBD version.
1228

1229
    This will return a dict with keys:
1230
      - k_major
1231
      - k_minor
1232
      - k_point
1233
      - api
1234
      - proto
1235
      - proto2 (only on drbd > 8.2.X)
1236

1237
    """
1238
    first_line = proc_data[0].strip()
1239
    version = cls._VERSION_RE.match(first_line)
1240
    if not version:
1241
      raise errors.BlockDeviceError("Can't parse DRBD version from '%s'" %
1242
                                    first_line)
1243

    
1244
    values = version.groups()
1245
    retval = {
1246
      "k_major": int(values[0]),
1247
      "k_minor": int(values[1]),
1248
      "k_point": int(values[2]),
1249
      "api": int(values[3]),
1250
      "proto": int(values[4]),
1251
      }
1252
    if values[5] is not None:
1253
      retval["proto2"] = values[5]
1254

    
1255
    return retval
1256

    
1257
  @staticmethod
1258
  def GetUsermodeHelper(filename=_USERMODE_HELPER_FILE):
1259
    """Returns DRBD usermode_helper currently set.
1260

1261
    """
1262
    try:
1263
      helper = utils.ReadFile(filename).splitlines()[0]
1264
    except EnvironmentError, err:
1265
      if err.errno == errno.ENOENT:
1266
        _ThrowError("The file %s cannot be opened, check if the module"
1267
                    " is loaded (%s)", filename, str(err))
1268
      else:
1269
        _ThrowError("Can't read DRBD helper file %s: %s", filename, str(err))
1270
    if not helper:
1271
      _ThrowError("Can't read any data from %s", filename)
1272
    return helper
1273

    
1274
  @staticmethod
1275
  def _DevPath(minor):
1276
    """Return the path to a drbd device for a given minor.
1277

1278
    """
1279
    return "/dev/drbd%d" % minor
1280

    
1281
  @classmethod
1282
  def GetUsedDevs(cls):
1283
    """Compute the list of used DRBD devices.
1284

1285
    """
1286
    data = cls._GetProcData()
1287

    
1288
    used_devs = {}
1289
    for line in data:
1290
      match = cls._VALID_LINE_RE.match(line)
1291
      if not match:
1292
        continue
1293
      minor = int(match.group(1))
1294
      state = match.group(2)
1295
      if state == cls._ST_UNCONFIGURED:
1296
        continue
1297
      used_devs[minor] = state, line
1298

    
1299
    return used_devs
1300

    
1301
  def _SetFromMinor(self, minor):
1302
    """Set our parameters based on the given minor.
1303

1304
    This sets our minor variable and our dev_path.
1305

1306
    """
1307
    if minor is None:
1308
      self.minor = self.dev_path = None
1309
      self.attached = False
1310
    else:
1311
      self.minor = minor
1312
      self.dev_path = self._DevPath(minor)
1313
      self.attached = True
1314

    
1315
  @staticmethod
1316
  def _CheckMetaSize(meta_device):
1317
    """Check if the given meta device looks like a valid one.
1318

1319
    This currently only checks the size, which must be around
1320
    128MiB.
1321

1322
    """
1323
    result = utils.RunCmd(["blockdev", "--getsize", meta_device])
1324
    if result.failed:
1325
      _ThrowError("Failed to get device size: %s - %s",
1326
                  result.fail_reason, result.output)
1327
    try:
1328
      sectors = int(result.stdout)
1329
    except (TypeError, ValueError):
1330
      _ThrowError("Invalid output from blockdev: '%s'", result.stdout)
1331
    num_bytes = sectors * 512
1332
    if num_bytes < 128 * 1024 * 1024: # less than 128MiB
1333
      _ThrowError("Meta device too small (%.2fMib)", (num_bytes / 1024 / 1024))
1334
    # the maximum *valid* size of the meta device when living on top
1335
    # of LVM is hard to compute: it depends on the number of stripes
1336
    # and the PE size; e.g. a 2-stripe, 64MB PE will result in a 128MB
1337
    # (normal size), but an eight-stripe 128MB PE will result in a 1GB
1338
    # size meta device; as such, we restrict it to 1GB (a little bit
1339
    # too generous, but making assumptions about PE size is hard)
1340
    if num_bytes > 1024 * 1024 * 1024:
1341
      _ThrowError("Meta device too big (%.2fMiB)", (num_bytes / 1024 / 1024))
1342

    
1343
  def Rename(self, new_id):
1344
    """Rename a device.
1345

1346
    This is not supported for drbd devices.
1347

1348
    """
1349
    raise errors.ProgrammerError("Can't rename a drbd device")
1350

    
1351

    
1352
class DRBD8(BaseDRBD):
1353
  """DRBD v8.x block device.
1354

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

1359
  The unique_id for the drbd device is a (local_ip, local_port,
1360
  remote_ip, remote_port, local_minor, secret) tuple, and it must have
1361
  two children: the data device and the meta_device. The meta device
1362
  is checked for valid size and is zeroed on create.
1363

1364
  """
1365
  _MAX_MINORS = 255
1366
  _PARSE_SHOW = None
1367

    
1368
  # timeout constants
1369
  _NET_RECONFIG_TIMEOUT = 60
1370

    
1371
  # command line options for barriers
1372
  _DISABLE_DISK_OPTION = "--no-disk-barrier"  # -a
1373
  _DISABLE_DRAIN_OPTION = "--no-disk-drain"   # -D
1374
  _DISABLE_FLUSH_OPTION = "--no-disk-flushes" # -i
1375
  _DISABLE_META_FLUSH_OPTION = "--no-md-flushes"  # -m
1376

    
1377
  def __init__(self, unique_id, children, size, params):
1378
    if children and children.count(None) > 0:
1379
      children = []
1380
    if len(children) not in (0, 2):
1381
      raise ValueError("Invalid configuration data %s" % str(children))
1382
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 6:
1383
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1384
    (self._lhost, self._lport,
1385
     self._rhost, self._rport,
1386
     self._aminor, self._secret) = unique_id
1387
    if children:
1388
      if not _CanReadDevice(children[1].dev_path):
1389
        logging.info("drbd%s: Ignoring unreadable meta device", self._aminor)
1390
        children = []
1391
    super(DRBD8, self).__init__(unique_id, children, size, params)
1392
    self.major = self._DRBD_MAJOR
1393
    version = self._GetVersion(self._GetProcData())
1394
    if version["k_major"] != 8:
1395
      _ThrowError("Mismatch in DRBD kernel version and requested ganeti"
1396
                  " usage: kernel is %s.%s, ganeti wants 8.x",
1397
                  version["k_major"], version["k_minor"])
1398

    
1399
    if (self._lhost is not None and self._lhost == self._rhost and
1400
        self._lport == self._rport):
1401
      raise ValueError("Invalid configuration data, same local/remote %s" %
1402
                       (unique_id,))
1403
    self.Attach()
1404

    
1405
  @classmethod
1406
  def _InitMeta(cls, minor, dev_path):
1407
    """Initialize a meta device.
1408

1409
    This will not work if the given minor is in use.
1410

1411
    """
1412
    # Zero the metadata first, in order to make sure drbdmeta doesn't
1413
    # try to auto-detect existing filesystems or similar (see
1414
    # http://code.google.com/p/ganeti/issues/detail?id=182); we only
1415
    # care about the first 128MB of data in the device, even though it
1416
    # can be bigger
1417
    result = utils.RunCmd([constants.DD_CMD,
1418
                           "if=/dev/zero", "of=%s" % dev_path,
1419
                           "bs=1048576", "count=128", "oflag=direct"])
1420
    if result.failed:
1421
      _ThrowError("Can't wipe the meta device: %s", result.output)
1422

    
1423
    result = utils.RunCmd(["drbdmeta", "--force", cls._DevPath(minor),
1424
                           "v08", dev_path, "0", "create-md"])
1425
    if result.failed:
1426
      _ThrowError("Can't initialize meta device: %s", result.output)
1427

    
1428
  @classmethod
1429
  def _FindUnusedMinor(cls):
1430
    """Find an unused DRBD device.
1431

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

1435
    """
1436
    data = cls._GetProcData()
1437

    
1438
    highest = None
1439
    for line in data:
1440
      match = cls._UNUSED_LINE_RE.match(line)
1441
      if match:
1442
        return int(match.group(1))
1443
      match = cls._VALID_LINE_RE.match(line)
1444
      if match:
1445
        minor = int(match.group(1))
1446
        highest = max(highest, minor)
1447
    if highest is None: # there are no minors in use at all
1448
      return 0
1449
    if highest >= cls._MAX_MINORS:
1450
      logging.error("Error: no free drbd minors!")
1451
      raise errors.BlockDeviceError("Can't find a free DRBD minor")
1452
    return highest + 1
1453

    
1454
  @classmethod
1455
  def _GetShowParser(cls):
1456
    """Return a parser for `drbd show` output.
1457

1458
    This will either create or return an already-created parser for the
1459
    output of the command `drbd show`.
1460

1461
    """
1462
    if cls._PARSE_SHOW is not None:
1463
      return cls._PARSE_SHOW
1464

    
1465
    # pyparsing setup
1466
    lbrace = pyp.Literal("{").suppress()
1467
    rbrace = pyp.Literal("}").suppress()
1468
    lbracket = pyp.Literal("[").suppress()
1469
    rbracket = pyp.Literal("]").suppress()
1470
    semi = pyp.Literal(";").suppress()
1471
    colon = pyp.Literal(":").suppress()
1472
    # this also converts the value to an int
1473
    number = pyp.Word(pyp.nums).setParseAction(lambda s, l, t: int(t[0]))
1474

    
1475
    comment = pyp.Literal("#") + pyp.Optional(pyp.restOfLine)
1476
    defa = pyp.Literal("_is_default").suppress()
1477
    dbl_quote = pyp.Literal('"').suppress()
1478

    
1479
    keyword = pyp.Word(pyp.alphanums + "-")
1480

    
1481
    # value types
1482
    value = pyp.Word(pyp.alphanums + "_-/.:")
1483
    quoted = dbl_quote + pyp.CharsNotIn('"') + dbl_quote
1484
    ipv4_addr = (pyp.Optional(pyp.Literal("ipv4")).suppress() +
1485
                 pyp.Word(pyp.nums + ".") + colon + number)
1486
    ipv6_addr = (pyp.Optional(pyp.Literal("ipv6")).suppress() +
1487
                 pyp.Optional(lbracket) + pyp.Word(pyp.hexnums + ":") +
1488
                 pyp.Optional(rbracket) + colon + number)
1489
    # meta device, extended syntax
1490
    meta_value = ((value ^ quoted) + lbracket + number + rbracket)
1491
    # device name, extended syntax
1492
    device_value = pyp.Literal("minor").suppress() + number
1493

    
1494
    # a statement
1495
    stmt = (~rbrace + keyword + ~lbrace +
1496
            pyp.Optional(ipv4_addr ^ ipv6_addr ^ value ^ quoted ^ meta_value ^
1497
                         device_value) +
1498
            pyp.Optional(defa) + semi +
1499
            pyp.Optional(pyp.restOfLine).suppress())
1500

    
1501
    # an entire section
1502
    section_name = pyp.Word(pyp.alphas + "_")
1503
    section = section_name + lbrace + pyp.ZeroOrMore(pyp.Group(stmt)) + rbrace
1504

    
1505
    bnf = pyp.ZeroOrMore(pyp.Group(section ^ stmt))
1506
    bnf.ignore(comment)
1507

    
1508
    cls._PARSE_SHOW = bnf
1509

    
1510
    return bnf
1511

    
1512
  @classmethod
1513
  def _GetShowData(cls, minor):
1514
    """Return the `drbdsetup show` data for a minor.
1515

1516
    """
1517
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "show"])
1518
    if result.failed:
1519
      logging.error("Can't display the drbd config: %s - %s",
1520
                    result.fail_reason, result.output)
1521
      return None
1522
    return result.stdout
1523

    
1524
  @classmethod
1525
  def _GetDevInfo(cls, out):
1526
    """Parse details about a given DRBD minor.
1527

1528
    This return, if available, the local backing device (as a path)
1529
    and the local and remote (ip, port) information from a string
1530
    containing the output of the `drbdsetup show` command as returned
1531
    by _GetShowData.
1532

1533
    """
1534
    data = {}
1535
    if not out:
1536
      return data
1537

    
1538
    bnf = cls._GetShowParser()
1539
    # run pyparse
1540

    
1541
    try:
1542
      results = bnf.parseString(out)
1543
    except pyp.ParseException, err:
1544
      _ThrowError("Can't parse drbdsetup show output: %s", str(err))
1545

    
1546
    # and massage the results into our desired format
1547
    for section in results:
1548
      sname = section[0]
1549
      if sname == "_this_host":
1550
        for lst in section[1:]:
1551
          if lst[0] == "disk":
1552
            data["local_dev"] = lst[1]
1553
          elif lst[0] == "meta-disk":
1554
            data["meta_dev"] = lst[1]
1555
            data["meta_index"] = lst[2]
1556
          elif lst[0] == "address":
1557
            data["local_addr"] = tuple(lst[1:])
1558
      elif sname == "_remote_host":
1559
        for lst in section[1:]:
1560
          if lst[0] == "address":
1561
            data["remote_addr"] = tuple(lst[1:])
1562
    return data
1563

    
1564
  def _MatchesLocal(self, info):
1565
    """Test if our local config matches with an existing device.
1566

1567
    The parameter should be as returned from `_GetDevInfo()`. This
1568
    method tests if our local backing device is the same as the one in
1569
    the info parameter, in effect testing if we look like the given
1570
    device.
1571

1572
    """
1573
    if self._children:
1574
      backend, meta = self._children
1575
    else:
1576
      backend = meta = None
1577

    
1578
    if backend is not None:
1579
      retval = ("local_dev" in info and info["local_dev"] == backend.dev_path)
1580
    else:
1581
      retval = ("local_dev" not in info)
1582

    
1583
    if meta is not None:
1584
      retval = retval and ("meta_dev" in info and
1585
                           info["meta_dev"] == meta.dev_path)
1586
      retval = retval and ("meta_index" in info and
1587
                           info["meta_index"] == 0)
1588
    else:
1589
      retval = retval and ("meta_dev" not in info and
1590
                           "meta_index" not in info)
1591
    return retval
1592

    
1593
  def _MatchesNet(self, info):
1594
    """Test if our network config matches with an existing device.
1595

1596
    The parameter should be as returned from `_GetDevInfo()`. This
1597
    method tests if our network configuration is the same as the one
1598
    in the info parameter, in effect testing if we look like the given
1599
    device.
1600

1601
    """
1602
    if (((self._lhost is None and not ("local_addr" in info)) and
1603
         (self._rhost is None and not ("remote_addr" in info)))):
1604
      return True
1605

    
1606
    if self._lhost is None:
1607
      return False
1608

    
1609
    if not ("local_addr" in info and
1610
            "remote_addr" in info):
1611
      return False
1612

    
1613
    retval = (info["local_addr"] == (self._lhost, self._lport))
1614
    retval = (retval and
1615
              info["remote_addr"] == (self._rhost, self._rport))
1616
    return retval
1617

    
1618
  def _AssembleLocal(self, minor, backend, meta, size):
1619
    """Configure the local part of a DRBD device.
1620

1621
    """
1622
    args = ["drbdsetup", self._DevPath(minor), "disk",
1623
            backend, meta, "0",
1624
            "-e", "detach",
1625
            "--create-device"]
1626
    if size:
1627
      args.extend(["-d", "%sm" % size])
1628

    
1629
    version = self._GetVersion(self._GetProcData())
1630
    vmaj = version["k_major"]
1631
    vmin = version["k_minor"]
1632
    vrel = version["k_point"]
1633

    
1634
    barrier_args = \
1635
      self._ComputeDiskBarrierArgs(vmaj, vmin, vrel,
1636
                                   self.params[constants.LDP_BARRIERS],
1637
                                   self.params[constants.LDP_NO_META_FLUSH])
1638
    args.extend(barrier_args)
1639

    
1640
    if self.params[constants.LDP_DISK_CUSTOM]:
1641
      args.extend(shlex.split(self.params[constants.LDP_DISK_CUSTOM]))
1642

    
1643
    result = utils.RunCmd(args)
1644
    if result.failed:
1645
      _ThrowError("drbd%d: can't attach local disk: %s", minor, result.output)
1646

    
1647
  @classmethod
1648
  def _ComputeDiskBarrierArgs(cls, vmaj, vmin, vrel, disabled_barriers,
1649
                              disable_meta_flush):
1650
    """Compute the DRBD command line parameters for disk barriers
1651

1652
    Returns a list of the disk barrier parameters as requested via the
1653
    disabled_barriers and disable_meta_flush arguments, and according to the
1654
    supported ones in the DRBD version vmaj.vmin.vrel
1655

1656
    If the desired option is unsupported, raises errors.BlockDeviceError.
1657

1658
    """
1659
    disabled_barriers_set = frozenset(disabled_barriers)
1660
    if not disabled_barriers_set in constants.DRBD_VALID_BARRIER_OPT:
1661
      raise errors.BlockDeviceError("%s is not a valid option set for DRBD"
1662
                                    " barriers" % disabled_barriers)
1663

    
1664
    args = []
1665

    
1666
    # The following code assumes DRBD 8.x, with x < 4 and x != 1 (DRBD 8.1.x
1667
    # does not exist)
1668
    if not vmaj == 8 and vmin in (0, 2, 3):
1669
      raise errors.BlockDeviceError("Unsupported DRBD version: %d.%d.%d" %
1670
                                    (vmaj, vmin, vrel))
1671

    
1672
    def _AppendOrRaise(option, min_version):
1673
      """Helper for DRBD options"""
1674
      if min_version is not None and vrel >= min_version:
1675
        args.append(option)
1676
      else:
1677
        raise errors.BlockDeviceError("Could not use the option %s as the"
1678
                                      " DRBD version %d.%d.%d does not support"
1679
                                      " it." % (option, vmaj, vmin, vrel))
1680

    
1681
    # the minimum version for each feature is encoded via pairs of (minor
1682
    # version -> x) where x is version in which support for the option was
1683
    # introduced.
1684
    meta_flush_supported = disk_flush_supported = {
1685
      0: 12,
1686
      2: 7,
1687
      3: 0,
1688
      }
1689

    
1690
    disk_drain_supported = {
1691
      2: 7,
1692
      3: 0,
1693
      }
1694

    
1695
    disk_barriers_supported = {
1696
      3: 0,
1697
      }
1698

    
1699
    # meta flushes
1700
    if disable_meta_flush:
1701
      _AppendOrRaise(cls._DISABLE_META_FLUSH_OPTION,
1702
                     meta_flush_supported.get(vmin, None))
1703

    
1704
    # disk flushes
1705
    if constants.DRBD_B_DISK_FLUSH in disabled_barriers_set:
1706
      _AppendOrRaise(cls._DISABLE_FLUSH_OPTION,
1707
                     disk_flush_supported.get(vmin, None))
1708

    
1709
    # disk drain
1710
    if constants.DRBD_B_DISK_DRAIN in disabled_barriers_set:
1711
      _AppendOrRaise(cls._DISABLE_DRAIN_OPTION,
1712
                     disk_drain_supported.get(vmin, None))
1713

    
1714
    # disk barriers
1715
    if constants.DRBD_B_DISK_BARRIERS in disabled_barriers_set:
1716
      _AppendOrRaise(cls._DISABLE_DISK_OPTION,
1717
                     disk_barriers_supported.get(vmin, None))
1718

    
1719
    return args
1720

    
1721
  def _AssembleNet(self, minor, net_info, protocol,
1722
                   dual_pri=False, hmac=None, secret=None):
1723
    """Configure the network part of the device.
1724

1725
    """
1726
    lhost, lport, rhost, rport = net_info
1727
    if None in net_info:
1728
      # we don't want network connection and actually want to make
1729
      # sure its shutdown
1730
      self._ShutdownNet(minor)
1731
      return
1732

    
1733
    # Workaround for a race condition. When DRBD is doing its dance to
1734
    # establish a connection with its peer, it also sends the
1735
    # synchronization speed over the wire. In some cases setting the
1736
    # sync speed only after setting up both sides can race with DRBD
1737
    # connecting, hence we set it here before telling DRBD anything
1738
    # about its peer.
1739
    sync_errors = self._SetMinorSyncParams(minor, self.params)
1740
    if sync_errors:
1741
      _ThrowError("drbd%d: can't set the synchronization parameters: %s" %
1742
                  (minor, utils.CommaJoin(sync_errors)))
1743

    
1744
    if netutils.IP6Address.IsValid(lhost):
1745
      if not netutils.IP6Address.IsValid(rhost):
1746
        _ThrowError("drbd%d: can't connect ip %s to ip %s" %
1747
                    (minor, lhost, rhost))
1748
      family = "ipv6"
1749
    elif netutils.IP4Address.IsValid(lhost):
1750
      if not netutils.IP4Address.IsValid(rhost):
1751
        _ThrowError("drbd%d: can't connect ip %s to ip %s" %
1752
                    (minor, lhost, rhost))
1753
      family = "ipv4"
1754
    else:
1755
      _ThrowError("drbd%d: Invalid ip %s" % (minor, lhost))
1756

    
1757
    args = ["drbdsetup", self._DevPath(minor), "net",
1758
            "%s:%s:%s" % (family, lhost, lport),
1759
            "%s:%s:%s" % (family, rhost, rport), protocol,
1760
            "-A", "discard-zero-changes",
1761
            "-B", "consensus",
1762
            "--create-device",
1763
            ]
1764
    if dual_pri:
1765
      args.append("-m")
1766
    if hmac and secret:
1767
      args.extend(["-a", hmac, "-x", secret])
1768

    
1769
    if self.params[constants.LDP_NET_CUSTOM]:
1770
      args.extend(shlex.split(self.params[constants.LDP_NET_CUSTOM]))
1771

    
1772
    result = utils.RunCmd(args)
1773
    if result.failed:
1774
      _ThrowError("drbd%d: can't setup network: %s - %s",
1775
                  minor, result.fail_reason, result.output)
1776

    
1777
    def _CheckNetworkConfig():
1778
      info = self._GetDevInfo(self._GetShowData(minor))
1779
      if not "local_addr" in info or not "remote_addr" in info:
1780
        raise utils.RetryAgain()
1781

    
1782
      if (info["local_addr"] != (lhost, lport) or
1783
          info["remote_addr"] != (rhost, rport)):
1784
        raise utils.RetryAgain()
1785

    
1786
    try:
1787
      utils.Retry(_CheckNetworkConfig, 1.0, 10.0)
1788
    except utils.RetryTimeout:
1789
      _ThrowError("drbd%d: timeout while configuring network", minor)
1790

    
1791
  def AddChildren(self, devices):
1792
    """Add a disk to the DRBD device.
1793

1794
    """
1795
    if self.minor is None:
1796
      _ThrowError("drbd%d: can't attach to dbrd8 during AddChildren",
1797
                  self._aminor)
1798
    if len(devices) != 2:
1799
      _ThrowError("drbd%d: need two devices for AddChildren", self.minor)
1800
    info = self._GetDevInfo(self._GetShowData(self.minor))
1801
    if "local_dev" in info:
1802
      _ThrowError("drbd%d: already attached to a local disk", self.minor)
1803
    backend, meta = devices
1804
    if backend.dev_path is None or meta.dev_path is None:
1805
      _ThrowError("drbd%d: children not ready during AddChildren", self.minor)
1806
    backend.Open()
1807
    meta.Open()
1808
    self._CheckMetaSize(meta.dev_path)
1809
    self._InitMeta(self._FindUnusedMinor(), meta.dev_path)
1810

    
1811
    self._AssembleLocal(self.minor, backend.dev_path, meta.dev_path, self.size)
1812
    self._children = devices
1813

    
1814
  def RemoveChildren(self, devices):
1815
    """Detach the drbd device from local storage.
1816

1817
    """
1818
    if self.minor is None:
1819
      _ThrowError("drbd%d: can't attach to drbd8 during RemoveChildren",
1820
                  self._aminor)
1821
    # early return if we don't actually have backing storage
1822
    info = self._GetDevInfo(self._GetShowData(self.minor))
1823
    if "local_dev" not in info:
1824
      return
1825
    if len(self._children) != 2:
1826
      _ThrowError("drbd%d: we don't have two children: %s", self.minor,
1827
                  self._children)
1828
    if self._children.count(None) == 2: # we don't actually have children :)
1829
      logging.warning("drbd%d: requested detach while detached", self.minor)
1830
      return
1831
    if len(devices) != 2:
1832
      _ThrowError("drbd%d: we need two children in RemoveChildren", self.minor)
1833
    for child, dev in zip(self._children, devices):
1834
      if dev != child.dev_path:
1835
        _ThrowError("drbd%d: mismatch in local storage (%s != %s) in"
1836
                    " RemoveChildren", self.minor, dev, child.dev_path)
1837

    
1838
    self._ShutdownLocal(self.minor)
1839
    self._children = []
1840

    
1841
  @classmethod
1842
  def _SetMinorSyncParams(cls, minor, params):
1843
    """Set the parameters of the DRBD syncer.
1844

1845
    This is the low-level implementation.
1846

1847
    @type minor: int
1848
    @param minor: the drbd minor whose settings we change
1849
    @type params: dict
1850
    @param params: LD level disk parameters related to the synchronization
1851
    @rtype: list
1852
    @return: a list of error messages
1853

1854
    """
1855

    
1856
    args = ["drbdsetup", cls._DevPath(minor), "syncer"]
1857
    if params[constants.LDP_DYNAMIC_RESYNC]:
1858
      version = cls._GetVersion(cls._GetProcData())
1859
      vmin = version["k_minor"]
1860
      vrel = version["k_point"]
1861

    
1862
      # By definition we are using 8.x, so just check the rest of the version
1863
      # number
1864
      if vmin != 3 or vrel < 9:
1865
        msg = ("The current DRBD version (8.%d.%d) does not support the "
1866
               "dynamic resync speed controller" % (vmin, vrel))
1867
        logging.error(msg)
1868
        return [msg]
1869

    
1870
      if params[constants.LDP_PLAN_AHEAD] == 0:
1871
        msg = ("A value of 0 for c-plan-ahead disables the dynamic sync speed"
1872
               " controller at DRBD level. If you want to disable it, please"
1873
               " set the dynamic-resync disk parameter to False.")
1874
        logging.error(msg)
1875
        return [msg]
1876

    
1877
      # add the c-* parameters to args
1878
      args.extend(["--c-plan-ahead", params[constants.LDP_PLAN_AHEAD],
1879
                   "--c-fill-target", params[constants.LDP_FILL_TARGET],
1880
                   "--c-delay-target", params[constants.LDP_DELAY_TARGET],
1881
                   "--c-max-rate", params[constants.LDP_MAX_RATE],
1882
                   "--c-min-rate", params[constants.LDP_MIN_RATE],
1883
                   ])
1884

    
1885
    else:
1886
      args.extend(["-r", "%d" % params[constants.LDP_RESYNC_RATE]])
1887

    
1888
    args.append("--create-device")
1889
    result = utils.RunCmd(args)
1890
    if result.failed:
1891
      msg = ("Can't change syncer rate: %s - %s" %
1892
             (result.fail_reason, result.output))
1893
      logging.error(msg)
1894
      return [msg]
1895

    
1896
    return []
1897

    
1898
  def SetSyncParams(self, params):
1899
    """Set the synchronization parameters of the DRBD syncer.
1900

1901
    @type params: dict
1902
    @param params: LD level disk parameters related to the synchronization
1903
    @rtype: list
1904
    @return: a list of error messages, emitted both by the current node and by
1905
    children. An empty list means no errors
1906

1907
    """
1908
    if self.minor is None:
1909
      err = "Not attached during SetSyncParams"
1910
      logging.info(err)
1911
      return [err]
1912

    
1913
    children_result = super(DRBD8, self).SetSyncParams(params)
1914
    children_result.extend(self._SetMinorSyncParams(self.minor, params))
1915
    return children_result
1916

    
1917
  def PauseResumeSync(self, pause):
1918
    """Pauses or resumes the sync of a DRBD device.
1919

1920
    @param pause: Wether to pause or resume
1921
    @return: the success of the operation
1922

1923
    """
1924
    if self.minor is None:
1925
      logging.info("Not attached during PauseSync")
1926
      return False
1927

    
1928
    children_result = super(DRBD8, self).PauseResumeSync(pause)
1929

    
1930
    if pause:
1931
      cmd = "pause-sync"
1932
    else:
1933
      cmd = "resume-sync"
1934

    
1935
    result = utils.RunCmd(["drbdsetup", self.dev_path, cmd])
1936
    if result.failed:
1937
      logging.error("Can't %s: %s - %s", cmd,
1938
                    result.fail_reason, result.output)
1939
    return not result.failed and children_result
1940

    
1941
  def GetProcStatus(self):
1942
    """Return device data from /proc.
1943

1944
    """
1945
    if self.minor is None:
1946
      _ThrowError("drbd%d: GetStats() called while not attached", self._aminor)
1947
    proc_info = self._MassageProcData(self._GetProcData())
1948
    if self.minor not in proc_info:
1949
      _ThrowError("drbd%d: can't find myself in /proc", self.minor)
1950
    return DRBD8Status(proc_info[self.minor])
1951

    
1952
  def GetSyncStatus(self):
1953
    """Returns the sync status of the device.
1954

1955

1956
    If sync_percent is None, it means all is ok
1957
    If estimated_time is None, it means we can't estimate
1958
    the time needed, otherwise it's the time left in seconds.
1959

1960

1961
    We set the is_degraded parameter to True on two conditions:
1962
    network not connected or local disk missing.
1963

1964
    We compute the ldisk parameter based on whether we have a local
1965
    disk or not.
1966

1967
    @rtype: objects.BlockDevStatus
1968

1969
    """
1970
    if self.minor is None and not self.Attach():
1971
      _ThrowError("drbd%d: can't Attach() in GetSyncStatus", self._aminor)
1972

    
1973
    stats = self.GetProcStatus()
1974
    is_degraded = not stats.is_connected or not stats.is_disk_uptodate
1975

    
1976
    if stats.is_disk_uptodate:
1977
      ldisk_status = constants.LDS_OKAY
1978
    elif stats.is_diskless:
1979
      ldisk_status = constants.LDS_FAULTY
1980
    else:
1981
      ldisk_status = constants.LDS_UNKNOWN
1982

    
1983
    return objects.BlockDevStatus(dev_path=self.dev_path,
1984
                                  major=self.major,
1985
                                  minor=self.minor,
1986
                                  sync_percent=stats.sync_percent,
1987
                                  estimated_time=stats.est_time,
1988
                                  is_degraded=is_degraded,
1989
                                  ldisk_status=ldisk_status)
1990

    
1991
  def Open(self, force=False):
1992
    """Make the local state primary.
1993

1994
    If the 'force' parameter is given, the '-o' option is passed to
1995
    drbdsetup. Since this is a potentially dangerous operation, the
1996
    force flag should be only given after creation, when it actually
1997
    is mandatory.
1998

1999
    """
2000
    if self.minor is None and not self.Attach():
2001
      logging.error("DRBD cannot attach to a device during open")
2002
      return False
2003
    cmd = ["drbdsetup", self.dev_path, "primary"]
2004
    if force:
2005
      cmd.append("-o")
2006
    result = utils.RunCmd(cmd)
2007
    if result.failed:
2008
      _ThrowError("drbd%d: can't make drbd device primary: %s", self.minor,
2009
                  result.output)
2010

    
2011
  def Close(self):
2012
    """Make the local state secondary.
2013

2014
    This will, of course, fail if the device is in use.
2015

2016
    """
2017
    if self.minor is None and not self.Attach():
2018
      _ThrowError("drbd%d: can't Attach() in Close()", self._aminor)
2019
    result = utils.RunCmd(["drbdsetup", self.dev_path, "secondary"])
2020
    if result.failed:
2021
      _ThrowError("drbd%d: can't switch drbd device to secondary: %s",
2022
                  self.minor, result.output)
2023

    
2024
  def DisconnectNet(self):
2025
    """Removes network configuration.
2026

2027
    This method shutdowns the network side of the device.
2028

2029
    The method will wait up to a hardcoded timeout for the device to
2030
    go into standalone after the 'disconnect' command before
2031
    re-configuring it, as sometimes it takes a while for the
2032
    disconnect to actually propagate and thus we might issue a 'net'
2033
    command while the device is still connected. If the device will
2034
    still be attached to the network and we time out, we raise an
2035
    exception.
2036

2037
    """
2038
    if self.minor is None:
2039
      _ThrowError("drbd%d: disk not attached in re-attach net", self._aminor)
2040

    
2041
    if None in (self._lhost, self._lport, self._rhost, self._rport):
2042
      _ThrowError("drbd%d: DRBD disk missing network info in"
2043
                  " DisconnectNet()", self.minor)
2044

    
2045
    class _DisconnectStatus:
2046
      def __init__(self, ever_disconnected):
2047
        self.ever_disconnected = ever_disconnected
2048

    
2049
    dstatus = _DisconnectStatus(_IgnoreError(self._ShutdownNet, self.minor))
2050

    
2051
    def _WaitForDisconnect():
2052
      if self.GetProcStatus().is_standalone:
2053
        return
2054

    
2055
      # retry the disconnect, it seems possible that due to a well-time
2056
      # disconnect on the peer, my disconnect command might be ignored and
2057
      # forgotten
2058
      dstatus.ever_disconnected = \
2059
        _IgnoreError(self._ShutdownNet, self.minor) or dstatus.ever_disconnected
2060

    
2061
      raise utils.RetryAgain()
2062

    
2063
    # Keep start time
2064
    start_time = time.time()
2065

    
2066
    try:
2067
      # Start delay at 100 milliseconds and grow up to 2 seconds
2068
      utils.Retry(_WaitForDisconnect, (0.1, 1.5, 2.0),
2069
                  self._NET_RECONFIG_TIMEOUT)
2070
    except utils.RetryTimeout:
2071
      if dstatus.ever_disconnected:
2072
        msg = ("drbd%d: device did not react to the"
2073
               " 'disconnect' command in a timely manner")
2074
      else:
2075
        msg = "drbd%d: can't shutdown network, even after multiple retries"
2076

    
2077
      _ThrowError(msg, self.minor)
2078

    
2079
    reconfig_time = time.time() - start_time
2080
    if reconfig_time > (self._NET_RECONFIG_TIMEOUT * 0.25):
2081
      logging.info("drbd%d: DisconnectNet: detach took %.3f seconds",
2082
                   self.minor, reconfig_time)
2083

    
2084
  def AttachNet(self, multimaster):
2085
    """Reconnects the network.
2086

2087
    This method connects the network side of the device with a
2088
    specified multi-master flag. The device needs to be 'Standalone'
2089
    but have valid network configuration data.
2090

2091
    Args:
2092
      - multimaster: init the network in dual-primary mode
2093

2094
    """
2095
    if self.minor is None:
2096
      _ThrowError("drbd%d: device not attached in AttachNet", self._aminor)
2097

    
2098
    if None in (self._lhost, self._lport, self._rhost, self._rport):
2099
      _ThrowError("drbd%d: missing network info in AttachNet()", self.minor)
2100

    
2101
    status = self.GetProcStatus()
2102

    
2103
    if not status.is_standalone:
2104
      _ThrowError("drbd%d: device is not standalone in AttachNet", self.minor)
2105

    
2106
    self._AssembleNet(self.minor,
2107
                      (self._lhost, self._lport, self._rhost, self._rport),
2108
                      constants.DRBD_NET_PROTOCOL, dual_pri=multimaster,
2109
                      hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
2110

    
2111
  def Attach(self):
2112
    """Check if our minor is configured.
2113

2114
    This doesn't do any device configurations - it only checks if the
2115
    minor is in a state different from Unconfigured.
2116

2117
    Note that this function will not change the state of the system in
2118
    any way (except in case of side-effects caused by reading from
2119
    /proc).
2120

2121
    """
2122
    used_devs = self.GetUsedDevs()
2123
    if self._aminor in used_devs:
2124
      minor = self._aminor
2125
    else:
2126
      minor = None
2127

    
2128
    self._SetFromMinor(minor)
2129
    return minor is not None
2130

    
2131
  def Assemble(self):
2132
    """Assemble the drbd.
2133

2134
    Method:
2135
      - if we have a configured device, we try to ensure that it matches
2136
        our config
2137
      - if not, we create it from zero
2138
      - anyway, set the device parameters
2139

2140
    """
2141
    super(DRBD8, self).Assemble()
2142

    
2143
    self.Attach()
2144
    if self.minor is None:
2145
      # local device completely unconfigured
2146
      self._FastAssemble()
2147
    else:
2148
      # we have to recheck the local and network status and try to fix
2149
      # the device
2150
      self._SlowAssemble()
2151

    
2152
    sync_errors = self.SetSyncParams(self.params)
2153
    if sync_errors:
2154
      _ThrowError("drbd%d: can't set the synchronization parameters: %s" %
2155
                  (self.minor, utils.CommaJoin(sync_errors)))
2156

    
2157
  def _SlowAssemble(self):
2158
    """Assembles the DRBD device from a (partially) configured device.
2159

2160
    In case of partially attached (local device matches but no network
2161
    setup), we perform the network attach. If successful, we re-test
2162
    the attach if can return success.
2163

2164
    """
2165
    # TODO: Rewrite to not use a for loop just because there is 'break'
2166
    # pylint: disable=W0631
2167
    net_data = (self._lhost, self._lport, self._rhost, self._rport)
2168
    for minor in (self._aminor,):
2169
      info = self._GetDevInfo(self._GetShowData(minor))
2170
      match_l = self._MatchesLocal(info)
2171
      match_r = self._MatchesNet(info)
2172

    
2173
      if match_l and match_r:
2174
        # everything matches
2175
        break
2176

    
2177
      if match_l and not match_r and "local_addr" not in info:
2178
        # disk matches, but not attached to network, attach and recheck
2179
        self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
2180
                          hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
2181
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
2182
          break
2183
        else:
2184
          _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
2185
                      " show' disagrees", minor)
2186

    
2187
      if match_r and "local_dev" not in info:
2188
        # no local disk, but network attached and it matches
2189
        self._AssembleLocal(minor, self._children[0].dev_path,
2190
                            self._children[1].dev_path, self.size)
2191
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
2192
          break
2193
        else:
2194
          _ThrowError("drbd%d: disk attach successful, but 'drbdsetup"
2195
                      " show' disagrees", minor)
2196

    
2197
      # this case must be considered only if we actually have local
2198
      # storage, i.e. not in diskless mode, because all diskless
2199
      # devices are equal from the point of view of local
2200
      # configuration
2201
      if (match_l and "local_dev" in info and
2202
          not match_r and "local_addr" in info):
2203
        # strange case - the device network part points to somewhere
2204
        # else, even though its local storage is ours; as we own the
2205
        # drbd space, we try to disconnect from the remote peer and
2206
        # reconnect to our correct one
2207
        try:
2208
          self._ShutdownNet(minor)
2209
        except errors.BlockDeviceError, err:
2210
          _ThrowError("drbd%d: device has correct local storage, wrong"
2211
                      " remote peer and is unable to disconnect in order"
2212
                      " to attach to the correct peer: %s", minor, str(err))
2213
        # note: _AssembleNet also handles the case when we don't want
2214
        # local storage (i.e. one or more of the _[lr](host|port) is
2215
        # None)
2216
        self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
2217
                          hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
2218
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
2219
          break
2220
        else:
2221
          _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
2222
                      " show' disagrees", minor)
2223

    
2224
    else:
2225
      minor = None
2226

    
2227
    self._SetFromMinor(minor)
2228
    if minor is None:
2229
      _ThrowError("drbd%d: cannot activate, unknown or unhandled reason",
2230
                  self._aminor)
2231

    
2232
  def _FastAssemble(self):
2233
    """Assemble the drbd device from zero.
2234

2235
    This is run when in Assemble we detect our minor is unused.
2236

2237
    """
2238
    minor = self._aminor
2239
    if self._children and self._children[0] and self._children[1]:
2240
      self._AssembleLocal(minor, self._children[0].dev_path,
2241
                          self._children[1].dev_path, self.size)
2242
    if self._lhost and self._lport and self._rhost and self._rport:
2243
      self._AssembleNet(minor,
2244
                        (self._lhost, self._lport, self._rhost, self._rport),
2245
                        constants.DRBD_NET_PROTOCOL,
2246
                        hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
2247
    self._SetFromMinor(minor)
2248

    
2249
  @classmethod
2250
  def _ShutdownLocal(cls, minor):
2251
    """Detach from the local device.
2252

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

2256
    """
2257
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "detach"])
2258
    if result.failed:
2259
      _ThrowError("drbd%d: can't detach local disk: %s", minor, result.output)
2260

    
2261
  @classmethod
2262
  def _ShutdownNet(cls, minor):
2263
    """Disconnect from the remote peer.
2264

2265
    This fails if we don't have a local device.
2266

2267
    """
2268
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disconnect"])
2269
    if result.failed:
2270
      _ThrowError("drbd%d: can't shutdown network: %s", minor, result.output)
2271

    
2272
  @classmethod
2273
  def _ShutdownAll(cls, minor):
2274
    """Deactivate the device.
2275

2276
    This will, of course, fail if the device is in use.
2277

2278
    """
2279
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "down"])
2280
    if result.failed:
2281
      _ThrowError("drbd%d: can't shutdown drbd device: %s",
2282
                  minor, result.output)
2283

    
2284
  def Shutdown(self):
2285
    """Shutdown the DRBD device.
2286

2287
    """
2288
    if self.minor is None and not self.Attach():
2289
      logging.info("drbd%d: not attached during Shutdown()", self._aminor)
2290
      return
2291
    minor = self.minor
2292
    self.minor = None
2293
    self.dev_path = None
2294
    self._ShutdownAll(minor)
2295

    
2296
  def Remove(self):
2297
    """Stub remove for DRBD devices.
2298

2299
    """
2300
    self.Shutdown()
2301

    
2302
  @classmethod
2303
  def Create(cls, unique_id, children, size, params, excl_stor):
2304
    """Create a new DRBD8 device.
2305

2306
    Since DRBD devices are not created per se, just assembled, this
2307
    function only initializes the metadata.
2308

2309
    """
2310
    if len(children) != 2:
2311
      raise errors.ProgrammerError("Invalid setup for the drbd device")
2312
    if excl_stor:
2313
      raise errors.ProgrammerError("DRBD device requested with"
2314
                                   " exclusive_storage")
2315
    # check that the minor is unused
2316
    aminor = unique_id[4]
2317
    proc_info = cls._MassageProcData(cls._GetProcData())
2318
    if aminor in proc_info:
2319
      status = DRBD8Status(proc_info[aminor])
2320
      in_use = status.is_in_use
2321
    else:
2322
      in_use = False
2323
    if in_use:
2324
      _ThrowError("drbd%d: minor is already in use at Create() time", aminor)
2325
    meta = children[1]
2326
    meta.Assemble()
2327
    if not meta.Attach():
2328
      _ThrowError("drbd%d: can't attach to meta device '%s'",
2329
                  aminor, meta)
2330
    cls._CheckMetaSize(meta.dev_path)
2331
    cls._InitMeta(aminor, meta.dev_path)
2332
    return cls(unique_id, children, size, params)
2333

    
2334
  def Grow(self, amount, dryrun, backingstore):
2335
    """Resize the DRBD device and its backing storage.
2336

2337
    """
2338
    if self.minor is None:
2339
      _ThrowError("drbd%d: Grow called while not attached", self._aminor)
2340
    if len(self._children) != 2 or None in self._children:
2341
      _ThrowError("drbd%d: cannot grow diskless device", self.minor)
2342
    self._children[0].Grow(amount, dryrun, backingstore)
2343
    if dryrun or backingstore:
2344
      # DRBD does not support dry-run mode and is not backing storage,
2345
      # so we'll return here
2346
      return
2347
    result = utils.RunCmd(["drbdsetup", self.dev_path, "resize", "-s",
2348
                           "%dm" % (self.size + amount)])
2349
    if result.failed:
2350
      _ThrowError("drbd%d: resize failed: %s", self.minor, result.output)
2351

    
2352

    
2353
class FileStorage(BlockDev):
2354
  """File device.
2355

2356
  This class represents the a file storage backend device.
2357

2358
  The unique_id for the file device is a (file_driver, file_path) tuple.
2359

2360
  """
2361
  def __init__(self, unique_id, children, size, params):
2362
    """Initalizes a file device backend.
2363

2364
    """
2365
    if children:
2366
      raise errors.BlockDeviceError("Invalid setup for file device")
2367
    super(FileStorage, self).__init__(unique_id, children, size, params)
2368
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
2369
      raise ValueError("Invalid configuration data %s" % str(unique_id))
2370
    self.driver = unique_id[0]
2371
    self.dev_path = unique_id[1]
2372

    
2373
    CheckFileStoragePath(self.dev_path)
2374

    
2375
    self.Attach()
2376

    
2377
  def Assemble(self):
2378
    """Assemble the device.
2379

2380
    Checks whether the file device exists, raises BlockDeviceError otherwise.
2381

2382
    """
2383
    if not os.path.exists(self.dev_path):
2384
      _ThrowError("File device '%s' does not exist" % self.dev_path)
2385

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

2389
    This is a no-op for the file type, as we don't deactivate
2390
    the file on shutdown.
2391

2392
    """
2393
    pass
2394

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

2398
    This is a no-op for the file type.
2399

2400
    """
2401
    pass
2402

    
2403
  def Close(self):
2404
    """Notifies that the device will no longer be used for I/O.
2405

2406
    This is a no-op for the file type.
2407

2408
    """
2409
    pass
2410

    
2411
  def Remove(self):
2412
    """Remove the file backing the block device.
2413

2414
    @rtype: boolean
2415
    @return: True if the removal was successful
2416

2417
    """
2418
    try:
2419
      os.remove(self.dev_path)
2420
    except OSError, err:
2421
      if err.errno != errno.ENOENT:
2422
        _ThrowError("Can't remove file '%s': %s", self.dev_path, err)
2423

    
2424
  def Rename(self, new_id):
2425
    """Renames the file.
2426

2427
    """
2428
    # TODO: implement rename for file-based storage
2429
    _ThrowError("Rename is not supported for file-based storage")
2430

    
2431
  def Grow(self, amount, dryrun, backingstore):
2432
    """Grow the file
2433

2434
    @param amount: the amount (in mebibytes) to grow with
2435

2436
    """
2437
    if not backingstore:
2438
      return
2439
    # Check that the file exists
2440
    self.Assemble()
2441
    current_size = self.GetActualSize()
2442
    new_size = current_size + amount * 1024 * 1024
2443
    assert new_size > current_size, "Cannot Grow with a negative amount"
2444
    # We can't really simulate the growth
2445
    if dryrun:
2446
      return
2447
    try:
2448
      f = open(self.dev_path, "a+")
2449
      f.truncate(new_size)
2450
      f.close()
2451
    except EnvironmentError, err:
2452
      _ThrowError("Error in file growth: %", str(err))
2453

    
2454
  def Attach(self):
2455
    """Attach to an existing file.
2456

2457
    Check if this file already exists.
2458

2459
    @rtype: boolean
2460
    @return: True if file exists
2461

2462
    """
2463
    self.attached = os.path.exists(self.dev_path)
2464
    return self.attached
2465

    
2466
  def GetActualSize(self):
2467
    """Return the actual disk size.
2468

2469
    @note: the device needs to be active when this is called
2470

2471
    """
2472
    assert self.attached, "BlockDevice not attached in GetActualSize()"
2473
    try:
2474
      st = os.stat(self.dev_path)
2475
      return st.st_size
2476
    except OSError, err:
2477
      _ThrowError("Can't stat %s: %s", self.dev_path, err)
2478

    
2479
  @classmethod
2480
  def Create(cls, unique_id, children, size, params, excl_stor):
2481
    """Create a new file.
2482

2483
    @param size: the size of file in MiB
2484

2485
    @rtype: L{bdev.FileStorage}
2486
    @return: an instance of FileStorage
2487

2488
    """
2489
    if excl_stor:
2490
      raise errors.ProgrammerError("FileStorage device requested with"
2491
                                   " exclusive_storage")
2492
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
2493
      raise ValueError("Invalid configuration data %s" % str(unique_id))
2494

    
2495
    dev_path = unique_id[1]
2496

    
2497
    CheckFileStoragePath(dev_path)
2498

    
2499
    try:
2500
      fd = os.open(dev_path, os.O_RDWR | os.O_CREAT | os.O_EXCL)
2501
      f = os.fdopen(fd, "w")
2502
      f.truncate(size * 1024 * 1024)
2503
      f.close()
2504
    except EnvironmentError, err:
2505
      if err.errno == errno.EEXIST:
2506
        _ThrowError("File already existing: %s", dev_path)
2507
      _ThrowError("Error in file creation: %", str(err))
2508

    
2509
    return FileStorage(unique_id, children, size, params)
2510

    
2511

    
2512
class PersistentBlockDevice(BlockDev):
2513
  """A block device with persistent node
2514

2515
  May be either directly attached, or exposed through DM (e.g. dm-multipath).
2516
  udev helpers are probably required to give persistent, human-friendly
2517
  names.
2518

2519
  For the time being, pathnames are required to lie under /dev.
2520

2521
  """
2522
  def __init__(self, unique_id, children, size, params):
2523
    """Attaches to a static block device.
2524

2525
    The unique_id is a path under /dev.
2526

2527
    """
2528
    super(PersistentBlockDevice, self).__init__(unique_id, children, size,
2529
                                                params)
2530
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
2531
      raise ValueError("Invalid configuration data %s" % str(unique_id))
2532
    self.dev_path = unique_id[1]
2533
    if not os.path.realpath(self.dev_path).startswith("/dev/"):
2534
      raise ValueError("Full path '%s' lies outside /dev" %
2535
                              os.path.realpath(self.dev_path))
2536
    # TODO: this is just a safety guard checking that we only deal with devices
2537
    # we know how to handle. In the future this will be integrated with
2538
    # external storage backends and possible values will probably be collected
2539
    # from the cluster configuration.
2540
    if unique_id[0] != constants.BLOCKDEV_DRIVER_MANUAL:
2541
      raise ValueError("Got persistent block device of invalid type: %s" %
2542
                       unique_id[0])
2543

    
2544
    self.major = self.minor = None
2545
    self.Attach()
2546

    
2547
  @classmethod
2548
  def Create(cls, unique_id, children, size, params, excl_stor):
2549
    """Create a new device
2550

2551
    This is a noop, we only return a PersistentBlockDevice instance
2552

2553
    """
2554
    if excl_stor:
2555
      raise errors.ProgrammerError("Persistent block device requested with"
2556
                                   " exclusive_storage")
2557
    return PersistentBlockDevice(unique_id, children, 0, params)
2558

    
2559
  def Remove(self):
2560
    """Remove a device
2561

2562
    This is a noop
2563

2564
    """
2565
    pass
2566

    
2567
  def Rename(self, new_id):
2568
    """Rename this device.
2569

2570
    """
2571
    _ThrowError("Rename is not supported for PersistentBlockDev storage")
2572

    
2573
  def Attach(self):
2574
    """Attach to an existing block device.
2575

2576

2577
    """
2578
    self.attached = False
2579
    try:
2580
      st = os.stat(self.dev_path)
2581
    except OSError, err:
2582
      logging.error("Error stat()'ing %s: %s", self.dev_path, str(err))
2583
      return False
2584

    
2585
    if not stat.S_ISBLK(st.st_mode):
2586
      logging.error("%s is not a block device", self.dev_path)
2587
      return False
2588

    
2589
    self.major = os.major(st.st_rdev)
2590
    self.minor = os.minor(st.st_rdev)
2591
    self.attached = True
2592

    
2593
    return True
2594

    
2595
  def Assemble(self):
2596
    """Assemble the device.
2597

2598
    """
2599
    pass
2600

    
2601
  def Shutdown(self):
2602
    """Shutdown the device.
2603

2604
    """
2605
    pass
2606

    
2607
  def Open(self, force=False):
2608
    """Make the device ready for I/O.
2609

2610
    """
2611
    pass
2612

    
2613
  def Close(self):
2614
    """Notifies that the device will no longer be used for I/O.
2615

2616
    """
2617
    pass
2618

    
2619
  def Grow(self, amount, dryrun, backingstore):
2620
    """Grow the logical volume.
2621

2622
    """
2623
    _ThrowError("Grow is not supported for PersistentBlockDev storage")
2624

    
2625

    
2626
class RADOSBlockDevice(BlockDev):
2627
  """A RADOS Block Device (rbd).
2628

2629
  This class implements the RADOS Block Device for the backend. You need
2630
  the rbd kernel driver, the RADOS Tools and a working RADOS cluster for
2631
  this to be functional.
2632

2633
  """
2634
  def __init__(self, unique_id, children, size, params):
2635
    """Attaches to an rbd device.
2636

2637
    """
2638
    super(RADOSBlockDevice, self).__init__(unique_id, children, size, params)
2639
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
2640
      raise ValueError("Invalid configuration data %s" % str(unique_id))
2641

    
2642
    self.driver, self.rbd_name = unique_id
2643

    
2644
    self.major = self.minor = None
2645
    self.Attach()
2646

    
2647
  @classmethod
2648
  def Create(cls, unique_id, children, size, params, excl_stor):
2649
    """Create a new rbd device.
2650

2651
    Provision a new rbd volume inside a RADOS pool.
2652

2653
    """
2654
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
2655
      raise errors.ProgrammerError("Invalid configuration data %s" %
2656
                                   str(unique_id))
2657
    if excl_stor:
2658
      raise errors.ProgrammerError("RBD device requested with"
2659
                                   " exclusive_storage")
2660
    rbd_pool = params[constants.LDP_POOL]
2661
    rbd_name = unique_id[1]
2662

    
2663
    # Provision a new rbd volume (Image) inside the RADOS cluster.
2664
    cmd = [constants.RBD_CMD, "create", "-p", rbd_pool,
2665
           rbd_name, "--size", "%s" % size]
2666
    result = utils.RunCmd(cmd)
2667
    if result.failed:
2668
      _ThrowError("rbd creation failed (%s): %s",
2669
                  result.fail_reason, result.output)
2670

    
2671
    return RADOSBlockDevice(unique_id, children, size, params)
2672

    
2673
  def Remove(self):
2674
    """Remove the rbd device.
2675

2676
    """
2677
    rbd_pool = self.params[constants.LDP_POOL]
2678
    rbd_name = self.unique_id[1]
2679

    
2680
    if not self.minor and not self.Attach():
2681
      # The rbd device doesn't exist.
2682
      return
2683

    
2684
    # First shutdown the device (remove mappings).
2685
    self.Shutdown()
2686

    
2687
    # Remove the actual Volume (Image) from the RADOS cluster.
2688
    cmd = [constants.RBD_CMD, "rm", "-p", rbd_pool, rbd_name]
2689
    result = utils.RunCmd(cmd)
2690
    if result.failed:
2691
      _ThrowError("Can't remove Volume from cluster with rbd rm: %s - %s",
2692
                  result.fail_reason, result.output)
2693

    
2694
  def Rename(self, new_id):
2695
    """Rename this device.
2696

2697
    """
2698
    pass
2699

    
2700
  def Attach(self):
2701
    """Attach to an existing rbd device.
2702

2703
    This method maps the rbd volume that matches our name with
2704
    an rbd device and then attaches to this device.
2705

2706
    """
2707
    self.attached = False
2708

    
2709
    # Map the rbd volume to a block device under /dev
2710
    self.dev_path = self._MapVolumeToBlockdev(self.unique_id)
2711

    
2712
    try:
2713
      st = os.stat(self.dev_path)
2714
    except OSError, err:
2715
      logging.error("Error stat()'ing %s: %s", self.dev_path, str(err))
2716
      return False
2717

    
2718
    if not stat.S_ISBLK(st.st_mode):
2719
      logging.error("%s is not a block device", self.dev_path)
2720
      return False
2721

    
2722
    self.major = os.major(st.st_rdev)
2723
    self.minor = os.minor(st.st_rdev)
2724
    self.attached = True
2725

    
2726
    return True
2727

    
2728
  def _MapVolumeToBlockdev(self, unique_id):
2729
    """Maps existing rbd volumes to block devices.
2730

2731
    This method should be idempotent if the mapping already exists.
2732

2733
    @rtype: string
2734
    @return: the block device path that corresponds to the volume
2735

2736
    """
2737
    pool = self.params[constants.LDP_POOL]
2738
    name = unique_id[1]
2739

    
2740
    # Check if the mapping already exists.
2741
    rbd_dev = self._VolumeToBlockdev(pool, name)
2742
    if rbd_dev:
2743
      # The mapping exists. Return it.
2744
      return rbd_dev
2745

    
2746
    # The mapping doesn't exist. Create it.
2747
    map_cmd = [constants.RBD_CMD, "map", "-p", pool, name]
2748
    result = utils.RunCmd(map_cmd)
2749
    if result.failed:
2750
      _ThrowError("rbd map failed (%s): %s",
2751
                  result.fail_reason, result.output)
2752

    
2753
    # Find the corresponding rbd device.
2754
    rbd_dev = self._VolumeToBlockdev(pool, name)
2755
    if not rbd_dev:
2756
      _ThrowError("rbd map succeeded, but could not find the rbd block"
2757
                  " device in output of showmapped, for volume: %s", name)
2758

    
2759
    # The device was successfully mapped. Return it.
2760
    return rbd_dev
2761

    
2762
  @classmethod
2763
  def _VolumeToBlockdev(cls, pool, volume_name):
2764
    """Do the 'volume name'-to-'rbd block device' resolving.
2765

2766
    @type pool: string
2767
    @param pool: RADOS pool to use
2768
    @type volume_name: string
2769
    @param volume_name: the name of the volume whose device we search for
2770
    @rtype: string or None
2771
    @return: block device path if the volume is mapped, else None
2772

2773
    """
2774
    try:
2775
      # Newer versions of the rbd tool support json output formatting. Use it
2776
      # if available.
2777
      showmap_cmd = [
2778
        constants.RBD_CMD,
2779
        "showmapped",
2780
        "-p",
2781
        pool,
2782
        "--format",
2783
        "json"
2784
        ]
2785
      result = utils.RunCmd(showmap_cmd)
2786
      if result.failed:
2787
        logging.error("rbd JSON output formatting returned error (%s): %s,"
2788
                      "falling back to plain output parsing",
2789
                      result.fail_reason, result.output)
2790
        raise RbdShowmappedJsonError
2791

    
2792
      return cls._ParseRbdShowmappedJson(result.output, volume_name)
2793
    except RbdShowmappedJsonError:
2794
      # For older versions of rbd, we have to parse the plain / text output
2795
      # manually.
2796
      showmap_cmd = [constants.RBD_CMD, "showmapped", "-p", pool]
2797
      result = utils.RunCmd(showmap_cmd)
2798
      if result.failed:
2799
        _ThrowError("rbd showmapped failed (%s): %s",
2800
                    result.fail_reason, result.output)
2801

    
2802
      return cls._ParseRbdShowmappedPlain(result.output, volume_name)
2803

    
2804
  @staticmethod
2805
  def _ParseRbdShowmappedJson(output, volume_name):
2806
    """Parse the json output of `rbd showmapped'.
2807

2808
    This method parses the json output of `rbd showmapped' and returns the rbd
2809
    block device path (e.g. /dev/rbd0) that matches the given rbd volume.
2810

2811
    @type output: string
2812
    @param output: the json output of `rbd showmapped'
2813
    @type volume_name: string
2814
    @param volume_name: the name of the volume whose device we search for
2815
    @rtype: string or None
2816
    @return: block device path if the volume is mapped, else None
2817

2818
    """
2819
    try:
2820
      devices = serializer.LoadJson(output)
2821
    except ValueError, err:
2822
      _ThrowError("Unable to parse JSON data: %s" % err)
2823

    
2824
    rbd_dev = None
2825
    for d in devices.values(): # pylint: disable=E1103
2826
      try:
2827
        name = d["name"]
2828
      except KeyError:
2829
        _ThrowError("'name' key missing from json object %s", devices)
2830

    
2831
      if name == volume_name:
2832
        if rbd_dev is not None:
2833
          _ThrowError("rbd volume %s is mapped more than once", volume_name)
2834

    
2835
        rbd_dev = d["device"]
2836

    
2837
    return rbd_dev
2838

    
2839
  @staticmethod
2840
  def _ParseRbdShowmappedPlain(output, volume_name):
2841
    """Parse the (plain / text) output of `rbd showmapped'.
2842

2843
    This method parses the output of `rbd showmapped' and returns
2844
    the rbd block device path (e.g. /dev/rbd0) that matches the
2845
    given rbd volume.
2846

2847
    @type output: string
2848
    @param output: the plain text output of `rbd showmapped'
2849
    @type volume_name: string
2850
    @param volume_name: the name of the volume whose device we search for
2851
    @rtype: string or None
2852
    @return: block device path if the volume is mapped, else None
2853

2854
    """
2855
    allfields = 5
2856
    volumefield = 2
2857
    devicefield = 4
2858

    
2859
    lines = output.splitlines()
2860

    
2861
    # Try parsing the new output format (ceph >= 0.55).
2862
    splitted_lines = map(lambda l: l.split(), lines)
2863

    
2864
    # Check for empty output.
2865
    if not splitted_lines:
2866
      return None
2867

    
2868
    # Check showmapped output, to determine number of fields.
2869
    field_cnt = len(splitted_lines[0])
2870
    if field_cnt != allfields:
2871
      # Parsing the new format failed. Fallback to parsing the old output
2872
      # format (< 0.55).
2873
      splitted_lines = map(lambda l: l.split("\t"), lines)
2874
      if field_cnt != allfields:
2875
        _ThrowError("Cannot parse rbd showmapped output expected %s fields,"
2876
                    " found %s", allfields, field_cnt)
2877

    
2878
    matched_lines = \
2879
      filter(lambda l: len(l) == allfields and l[volumefield] == volume_name,
2880
             splitted_lines)
2881

    
2882
    if len(matched_lines) > 1:
2883
      _ThrowError("rbd volume %s mapped more than once", volume_name)
2884

    
2885
    if matched_lines:
2886
      # rbd block device found. Return it.
2887
      rbd_dev = matched_lines[0][devicefield]
2888
      return rbd_dev
2889

    
2890
    # The given volume is not mapped.
2891
    return None
2892

    
2893
  def Assemble(self):
2894
    """Assemble the device.
2895

2896
    """
2897
    pass
2898

    
2899
  def Shutdown(self):
2900
    """Shutdown the device.
2901

2902
    """
2903
    if not self.minor and not self.Attach():
2904
      # The rbd device doesn't exist.
2905
      return
2906

    
2907
    # Unmap the block device from the Volume.
2908
    self._UnmapVolumeFromBlockdev(self.unique_id)
2909

    
2910
    self.minor = None
2911
    self.dev_path = None
2912

    
2913
  def _UnmapVolumeFromBlockdev(self, unique_id):
2914
    """Unmaps the rbd device from the Volume it is mapped.
2915

2916
    Unmaps the rbd device from the Volume it was previously mapped to.
2917
    This method should be idempotent if the Volume isn't mapped.
2918

2919
    """
2920
    pool = self.params[constants.LDP_POOL]
2921
    name = unique_id[1]
2922

    
2923
    # Check if the mapping already exists.
2924
    rbd_dev = self._VolumeToBlockdev(pool, name)
2925

    
2926
    if rbd_dev:
2927
      # The mapping exists. Unmap the rbd device.
2928
      unmap_cmd = [constants.RBD_CMD, "unmap", "%s" % rbd_dev]
2929
      result = utils.RunCmd(unmap_cmd)
2930
      if result.failed:
2931
        _ThrowError("rbd unmap failed (%s): %s",
2932
                    result.fail_reason, result.output)
2933

    
2934
  def Open(self, force=False):
2935
    """Make the device ready for I/O.
2936

2937
    """
2938
    pass
2939

    
2940
  def Close(self):
2941
    """Notifies that the device will no longer be used for I/O.
2942

2943
    """
2944
    pass
2945

    
2946
  def Grow(self, amount, dryrun, backingstore):
2947
    """Grow the Volume.
2948

2949
    @type amount: integer
2950
    @param amount: the amount (in mebibytes) to grow with
2951
    @type dryrun: boolean
2952
    @param dryrun: whether to execute the operation in simulation mode
2953
        only, without actually increasing the size
2954

2955
    """
2956
    if not backingstore:
2957
      return
2958
    if not self.Attach():
2959
      _ThrowError("Can't attach to rbd device during Grow()")
2960

    
2961
    if dryrun:
2962
      # the rbd tool does not support dry runs of resize operations.
2963
      # Since rbd volumes are thinly provisioned, we assume
2964
      # there is always enough free space for the operation.
2965
      return
2966

    
2967
    rbd_pool = self.params[constants.LDP_POOL]
2968
    rbd_name = self.unique_id[1]
2969
    new_size = self.size + amount
2970

    
2971
    # Resize the rbd volume (Image) inside the RADOS cluster.
2972
    cmd = [constants.RBD_CMD, "resize", "-p", rbd_pool,
2973
           rbd_name, "--size", "%s" % new_size]
2974
    result = utils.RunCmd(cmd)
2975
    if result.failed:
2976
      _ThrowError("rbd resize failed (%s): %s",
2977
                  result.fail_reason, result.output)
2978

    
2979

    
2980
class ExtStorageDevice(BlockDev):
2981
  """A block device provided by an ExtStorage Provider.
2982

2983
  This class implements the External Storage Interface, which means
2984
  handling of the externally provided block devices.
2985

2986
  """
2987
  def __init__(self, unique_id, children, size, params):
2988
    """Attaches to an extstorage block device.
2989

2990
    """
2991
    super(ExtStorageDevice, self).__init__(unique_id, children, size, params)
2992
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
2993
      raise ValueError("Invalid configuration data %s" % str(unique_id))
2994

    
2995
    self.driver, self.vol_name = unique_id
2996
    self.ext_params = params
2997

    
2998
    self.major = self.minor = None
2999
    self.Attach()
3000

    
3001
  @classmethod
3002
  def Create(cls, unique_id, children, size, params, excl_stor):
3003
    """Create a new extstorage device.
3004

3005
    Provision a new volume using an extstorage provider, which will
3006
    then be mapped to a block device.
3007

3008
    """
3009
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
3010
      raise errors.ProgrammerError("Invalid configuration data %s" %
3011
                                   str(unique_id))
3012
    if excl_stor:
3013
      raise errors.ProgrammerError("extstorage device requested with"
3014
                                   " exclusive_storage")
3015

    
3016
    # Call the External Storage's create script,
3017
    # to provision a new Volume inside the External Storage
3018
    _ExtStorageAction(constants.ES_ACTION_CREATE, unique_id,
3019
                      params, str(size))
3020

    
3021
    return ExtStorageDevice(unique_id, children, size, params)
3022

    
3023
  def Remove(self):
3024
    """Remove the extstorage device.
3025

3026
    """
3027
    if not self.minor and not self.Attach():
3028
      # The extstorage device doesn't exist.
3029
      return
3030

    
3031
    # First shutdown the device (remove mappings).
3032
    self.Shutdown()
3033

    
3034
    # Call the External Storage's remove script,
3035
    # to remove the Volume from the External Storage
3036
    _ExtStorageAction(constants.ES_ACTION_REMOVE, self.unique_id,
3037
                      self.ext_params)
3038

    
3039
  def Rename(self, new_id):
3040
    """Rename this device.
3041

3042
    """
3043
    pass
3044

    
3045
  def Attach(self):
3046
    """Attach to an existing extstorage device.
3047

3048
    This method maps the extstorage volume that matches our name with
3049
    a corresponding block device and then attaches to this device.
3050

3051
    """
3052
    self.attached = False
3053

    
3054
    # Call the External Storage's attach script,
3055
    # to attach an existing Volume to a block device under /dev
3056
    self.dev_path = _ExtStorageAction(constants.ES_ACTION_ATTACH,
3057
                                      self.unique_id, self.ext_params)
3058

    
3059
    try:
3060
      st = os.stat(self.dev_path)
3061
    except OSError, err:
3062
      logging.error("Error stat()'ing %s: %s", self.dev_path, str(err))
3063
      return False
3064

    
3065
    if not stat.S_ISBLK(st.st_mode):
3066
      logging.error("%s is not a block device", self.dev_path)
3067
      return False
3068

    
3069
    self.major = os.major(st.st_rdev)
3070
    self.minor = os.minor(st.st_rdev)
3071
    self.attached = True
3072

    
3073
    return True
3074

    
3075
  def Assemble(self):
3076
    """Assemble the device.
3077

3078
    """
3079
    pass
3080

    
3081
  def Shutdown(self):
3082
    """Shutdown the device.
3083

3084
    """
3085
    if not self.minor and not self.Attach():
3086
      # The extstorage device doesn't exist.
3087
      return
3088

    
3089
    # Call the External Storage's detach script,
3090
    # to detach an existing Volume from it's block device under /dev
3091
    _ExtStorageAction(constants.ES_ACTION_DETACH, self.unique_id,
3092
                      self.ext_params)
3093

    
3094
    self.minor = None
3095
    self.dev_path = None
3096

    
3097
  def Open(self, force=False):
3098
    """Make the device ready for I/O.
3099

3100
    """
3101
    pass
3102

    
3103
  def Close(self):
3104
    """Notifies that the device will no longer be used for I/O.
3105

3106
    """
3107
    pass
3108

    
3109
  def Grow(self, amount, dryrun, backingstore):
3110
    """Grow the Volume.
3111

3112
    @type amount: integer
3113
    @param amount: the amount (in mebibytes) to grow with
3114
    @type dryrun: boolean
3115
    @param dryrun: whether to execute the operation in simulation mode
3116
        only, without actually increasing the size
3117

3118
    """
3119
    if not backingstore:
3120
      return
3121
    if not self.Attach():
3122
      _ThrowError("Can't attach to extstorage device during Grow()")
3123

    
3124
    if dryrun:
3125
      # we do not support dry runs of resize operations for now.
3126
      return
3127

    
3128
    new_size = self.size + amount
3129

    
3130
    # Call the External Storage's grow script,
3131
    # to grow an existing Volume inside the External Storage
3132
    _ExtStorageAction(constants.ES_ACTION_GROW, self.unique_id,
3133
                      self.ext_params, str(self.size), grow=str(new_size))
3134

    
3135
  def SetInfo(self, text):
3136
    """Update metadata with info text.
3137

3138
    """
3139
    # Replace invalid characters
3140
    text = re.sub("^[^A-Za-z0-9_+.]", "_", text)
3141
    text = re.sub("[^-A-Za-z0-9_+.]", "_", text)
3142

    
3143
    # Only up to 128 characters are allowed
3144
    text = text[:128]
3145

    
3146
    # Call the External Storage's setinfo script,
3147
    # to set metadata for an existing Volume inside the External Storage
3148
    _ExtStorageAction(constants.ES_ACTION_SETINFO, self.unique_id,
3149
                      self.ext_params, metadata=text)
3150

    
3151

    
3152
def _ExtStorageAction(action, unique_id, ext_params,
3153
                      size=None, grow=None, metadata=None):
3154
  """Take an External Storage action.
3155

3156
  Take an External Storage action concerning or affecting
3157
  a specific Volume inside the External Storage.
3158

3159
  @type action: string
3160
  @param action: which action to perform. One of:
3161
                 create / remove / grow / attach / detach
3162
  @type unique_id: tuple (driver, vol_name)
3163
  @param unique_id: a tuple containing the type of ExtStorage (driver)
3164
                    and the Volume name
3165
  @type ext_params: dict
3166
  @param ext_params: ExtStorage parameters
3167
  @type size: integer
3168
  @param size: the size of the Volume in mebibytes
3169
  @type grow: integer
3170
  @param grow: the new size in mebibytes (after grow)
3171
  @type metadata: string
3172
  @param metadata: metadata info of the Volume, for use by the provider
3173
  @rtype: None or a block device path (during attach)
3174

3175
  """
3176
  driver, vol_name = unique_id
3177

    
3178
  # Create an External Storage instance of type `driver'
3179
  status, inst_es = ExtStorageFromDisk(driver)
3180
  if not status:
3181
    _ThrowError("%s" % inst_es)
3182

    
3183
  # Create the basic environment for the driver's scripts
3184
  create_env = _ExtStorageEnvironment(unique_id, ext_params, size,
3185
                                      grow, metadata)
3186

    
3187
  # Do not use log file for action `attach' as we need
3188
  # to get the output from RunResult
3189
  # TODO: find a way to have a log file for attach too
3190
  logfile = None
3191
  if action is not constants.ES_ACTION_ATTACH:
3192
    logfile = _VolumeLogName(action, driver, vol_name)
3193

    
3194
  # Make sure the given action results in a valid script
3195
  if action not in constants.ES_SCRIPTS:
3196
    _ThrowError("Action '%s' doesn't result in a valid ExtStorage script" %
3197
                action)
3198

    
3199
  # Find out which external script to run according the given action
3200
  script_name = action + "_script"
3201
  script = getattr(inst_es, script_name)
3202

    
3203
  # Run the external script
3204
  result = utils.RunCmd([script], env=create_env,
3205
                        cwd=inst_es.path, output=logfile,)
3206
  if result.failed:
3207
    logging.error("External storage's %s command '%s' returned"
3208
                  " error: %s, logfile: %s, output: %s",
3209
                  action, result.cmd, result.fail_reason,
3210
                  logfile, result.output)
3211

    
3212
    # If logfile is 'None' (during attach), it breaks TailFile
3213
    # TODO: have a log file for attach too
3214
    if action is not constants.ES_ACTION_ATTACH:
3215
      lines = [utils.SafeEncode(val)
3216
               for val in utils.TailFile(logfile, lines=20)]
3217
    else:
3218
      lines = result.output[-20:]
3219

    
3220
    _ThrowError("External storage's %s script failed (%s), last"
3221
                " lines of output:\n%s",
3222
                action, result.fail_reason, "\n".join(lines))
3223

    
3224
  if action == constants.ES_ACTION_ATTACH:
3225
    return result.stdout
3226

    
3227

    
3228
def ExtStorageFromDisk(name, base_dir=None):
3229
  """Create an ExtStorage instance from disk.
3230

3231
  This function will return an ExtStorage instance
3232
  if the given name is a valid ExtStorage name.
3233

3234
  @type base_dir: string
3235
  @keyword base_dir: Base directory containing ExtStorage installations.
3236
                     Defaults to a search in all the ES_SEARCH_PATH dirs.
3237
  @rtype: tuple
3238
  @return: True and the ExtStorage instance if we find a valid one, or
3239
      False and the diagnose message on error
3240

3241
  """
3242
  if base_dir is None:
3243
    es_base_dir = pathutils.ES_SEARCH_PATH
3244
  else:
3245
    es_base_dir = [base_dir]
3246

    
3247
  es_dir = utils.FindFile(name, es_base_dir, os.path.isdir)
3248

    
3249
  if es_dir is None:
3250
    return False, ("Directory for External Storage Provider %s not"
3251
                   " found in search path" % name)
3252

    
3253
  # ES Files dictionary, we will populate it with the absolute path
3254
  # names; if the value is True, then it is a required file, otherwise
3255
  # an optional one
3256
  es_files = dict.fromkeys(constants.ES_SCRIPTS, True)
3257

    
3258
  es_files[constants.ES_PARAMETERS_FILE] = True
3259

    
3260
  for (filename, _) in es_files.items():
3261
    es_files[filename] = utils.PathJoin(es_dir, filename)
3262

    
3263
    try:
3264
      st = os.stat(es_files[filename])
3265
    except EnvironmentError, err:
3266
      return False, ("File '%s' under path '%s' is missing (%s)" %
3267
                     (filename, es_dir, utils.ErrnoOrStr(err)))
3268

    
3269
    if not stat.S_ISREG(stat.S_IFMT(st.st_mode)):
3270
      return False, ("File '%s' under path '%s' is not a regular file" %
3271
                     (filename, es_dir))
3272

    
3273
    if filename in constants.ES_SCRIPTS:
3274
      if stat.S_IMODE(st.st_mode) & stat.S_IXUSR != stat.S_IXUSR:
3275
        return False, ("File '%s' under path '%s' is not executable" %
3276
                       (filename, es_dir))
3277

    
3278
  parameters = []
3279
  if constants.ES_PARAMETERS_FILE in es_files:
3280
    parameters_file = es_files[constants.ES_PARAMETERS_FILE]
3281
    try:
3282
      parameters = utils.ReadFile(parameters_file).splitlines()
3283
    except EnvironmentError, err:
3284
      return False, ("Error while reading the EXT parameters file at %s: %s" %
3285
                     (parameters_file, utils.ErrnoOrStr(err)))
3286
    parameters = [v.split(None, 1) for v in parameters]
3287

    
3288
  es_obj = \
3289
    objects.ExtStorage(name=name, path=es_dir,
3290
                       create_script=es_files[constants.ES_SCRIPT_CREATE],
3291
                       remove_script=es_files[constants.ES_SCRIPT_REMOVE],
3292
                       grow_script=es_files[constants.ES_SCRIPT_GROW],
3293
                       attach_script=es_files[constants.ES_SCRIPT_ATTACH],
3294
                       detach_script=es_files[constants.ES_SCRIPT_DETACH],
3295
                       setinfo_script=es_files[constants.ES_SCRIPT_SETINFO],
3296
                       verify_script=es_files[constants.ES_SCRIPT_VERIFY],
3297
                       supported_parameters=parameters)
3298
  return True, es_obj
3299

    
3300

    
3301
def _ExtStorageEnvironment(unique_id, ext_params,
3302
                           size=None, grow=None, metadata=None):
3303
  """Calculate the environment for an External Storage script.
3304

3305
  @type unique_id: tuple (driver, vol_name)
3306
  @param unique_id: ExtStorage pool and name of the Volume
3307
  @type ext_params: dict
3308
  @param ext_params: the EXT parameters
3309
  @type size: string
3310
  @param size: size of the Volume (in mebibytes)
3311
  @type grow: string
3312
  @param grow: new size of Volume after grow (in mebibytes)
3313
  @type metadata: string
3314
  @param metadata: metadata info of the Volume
3315
  @rtype: dict
3316
  @return: dict of environment variables
3317

3318
  """
3319
  vol_name = unique_id[1]
3320

    
3321
  result = {}
3322
  result["VOL_NAME"] = vol_name
3323

    
3324
  # EXT params
3325
  for pname, pvalue in ext_params.items():
3326
    result["EXTP_%s" % pname.upper()] = str(pvalue)
3327

    
3328
  if size is not None:
3329
    result["VOL_SIZE"] = size
3330

    
3331
  if grow is not None:
3332
    result["VOL_NEW_SIZE"] = grow
3333

    
3334
  if metadata is not None:
3335
    result["VOL_METADATA"] = metadata
3336

    
3337
  return result
3338

    
3339

    
3340
def _VolumeLogName(kind, es_name, volume):
3341
  """Compute the ExtStorage log filename for a given Volume and operation.
3342

3343
  @type kind: string
3344
  @param kind: the operation type (e.g. create, remove etc.)
3345
  @type es_name: string
3346
  @param es_name: the ExtStorage name
3347
  @type volume: string
3348
  @param volume: the name of the Volume inside the External Storage
3349

3350
  """
3351
  # Check if the extstorage log dir is a valid dir
3352
  if not os.path.isdir(pathutils.LOG_ES_DIR):
3353
    _ThrowError("Cannot find log directory: %s", pathutils.LOG_ES_DIR)
3354

    
3355
  # TODO: Use tempfile.mkstemp to create unique filename
3356
  base = ("%s-%s-%s-%s.log" %
3357
          (kind, es_name, volume, utils.TimestampForFilename()))
3358
  return utils.PathJoin(pathutils.LOG_ES_DIR, base)
3359

    
3360

    
3361
DEV_MAP = {
3362
  constants.LD_LV: LogicalVolume,
3363
  constants.LD_DRBD8: DRBD8,
3364
  constants.LD_BLOCKDEV: PersistentBlockDevice,
3365
  constants.LD_RBD: RADOSBlockDevice,
3366
  constants.LD_EXT: ExtStorageDevice,
3367
  }
3368

    
3369
if constants.ENABLE_FILE_STORAGE or constants.ENABLE_SHARED_FILE_STORAGE:
3370
  DEV_MAP[constants.LD_FILE] = FileStorage
3371

    
3372

    
3373
def _VerifyDiskType(dev_type):
3374
  if dev_type not in DEV_MAP:
3375
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
3376

    
3377

    
3378
def _VerifyDiskParams(disk):
3379
  """Verifies if all disk parameters are set.
3380

3381
  """
3382
  missing = set(constants.DISK_LD_DEFAULTS[disk.dev_type]) - set(disk.params)
3383
  if missing:
3384
    raise errors.ProgrammerError("Block device is missing disk parameters: %s" %
3385
                                 missing)
3386

    
3387

    
3388
def FindDevice(disk, children):
3389
  """Search for an existing, assembled device.
3390

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

3394
  @type disk: L{objects.Disk}
3395
  @param disk: the disk object to find
3396
  @type children: list of L{bdev.BlockDev}
3397
  @param children: the list of block devices that are children of the device
3398
                  represented by the disk parameter
3399

3400
  """
3401
  _VerifyDiskType(disk.dev_type)
3402
  device = DEV_MAP[disk.dev_type](disk.physical_id, children, disk.size,
3403
                                  disk.params)
3404
  if not device.attached:
3405
    return None
3406
  return device
3407

    
3408

    
3409
def Assemble(disk, children):
3410
  """Try to attach or assemble an existing device.
3411

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

3415
  @type disk: L{objects.Disk}
3416
  @param disk: the disk object to assemble
3417
  @type children: list of L{bdev.BlockDev}
3418
  @param children: the list of block devices that are children of the device
3419
                  represented by the disk parameter
3420

3421
  """
3422
  _VerifyDiskType(disk.dev_type)
3423
  _VerifyDiskParams(disk)
3424
  device = DEV_MAP[disk.dev_type](disk.physical_id, children, disk.size,
3425
                                  disk.params)
3426
  device.Assemble()
3427
  return device
3428

    
3429

    
3430
def Create(disk, children, excl_stor):
3431
  """Create a device.
3432

3433
  @type disk: L{objects.Disk}
3434
  @param disk: the disk object to create
3435
  @type children: list of L{bdev.BlockDev}
3436
  @param children: the list of block devices that are children of the device
3437
                  represented by the disk parameter
3438
  @type excl_stor: boolean
3439
  @param excl_stor: Whether exclusive_storage is active
3440

3441
  """
3442
  _VerifyDiskType(disk.dev_type)
3443
  _VerifyDiskParams(disk)
3444
  device = DEV_MAP[disk.dev_type].Create(disk.physical_id, children, disk.size,
3445
                                         disk.params, excl_stor)
3446
  return device