bdev.py 71.6 KB
Newer Older
Iustin Pop's avatar
Iustin Pop committed
1
#
Iustin Pop's avatar
Iustin Pop committed
2 3
#

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


"""Block device abstraction"""

import re
import time
import errno
27
import stat
28
import pyparsing as pyp
Manuel Franceschini's avatar
Manuel Franceschini committed
29
import os
30
import logging
Iustin Pop's avatar
Iustin Pop committed
31 32 33

from ganeti import utils
from ganeti import errors
34
from ganeti import constants
35
from ganeti import objects
36
from ganeti import compat
37
from ganeti import netutils
Iustin Pop's avatar
Iustin Pop committed
38 39


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


44 45 46 47 48 49 50 51 52 53 54 55 56 57
def _IgnoreError(fn, *args, **kwargs):
  """Executes the given function, ignoring BlockDeviceErrors.

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

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

  """
  try:
    fn(*args, **kwargs)
    return True
  except errors.BlockDeviceError, err:
58
    logging.warning("Caught BlockDeviceError but ignoring: %s", str(err))
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
    return False


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

  @type msg: string
  @param msg: the text of the exception
  @raise errors.BlockDeviceError

  """
  if args:
    msg = msg % args
  logging.error(msg)
  raise errors.BlockDeviceError(msg)


76 77 78 79 80 81 82 83 84
def _CanReadDevice(path):
  """Check if we can read from the given device.

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

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


Iustin Pop's avatar
Iustin Pop committed
90 91 92 93 94 95 96 97 98 99
class BlockDev(object):
  """Block device abstract class.

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

  A device can also be online but read-only, however we are not using
100 101 102
  the readonly state (LV has it, if needed in the future) and we are
  usually looking at this like at a stack, so it's easier to
  conceptualise the transition from not-existing to online and back
Iustin Pop's avatar
Iustin Pop committed
103 104 105 106 107 108 109 110 111 112 113 114 115 116
  like a linear one.

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

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

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

  You can get to a device in two ways:
    - creating the (real) device, which returns you
123
      an attached instance (lvcreate)
Iustin Pop's avatar
Iustin Pop committed
124 125 126 127 128 129 130 131 132
    - attaching of a python instance to an existing (real) device

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

  """
Iustin Pop's avatar
Iustin Pop committed
133
  def __init__(self, unique_id, children, size):
Iustin Pop's avatar
Iustin Pop committed
134 135 136 137 138
    self._children = children
    self.dev_path = None
    self.unique_id = unique_id
    self.major = None
    self.minor = None
139
    self.attached = False
Iustin Pop's avatar
Iustin Pop committed
140
    self.size = size
Iustin Pop's avatar
Iustin Pop committed
141 142 143 144

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

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

    """
154
    pass
Iustin Pop's avatar
Iustin Pop committed
155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184

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

    """
    raise NotImplementedError

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

    """
    raise NotImplementedError

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

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

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

    """
    raise NotImplementedError

  def Remove(self):
    """Remove this device.

185
    This makes sense only for some of the device types: LV and file
Michael Hanselmann's avatar
Michael Hanselmann committed
186
    storage. Also note that if the device can't attach, the removal
187
    can't be completed.
Iustin Pop's avatar
Iustin Pop committed
188 189 190 191

    """
    raise NotImplementedError

Iustin Pop's avatar
Iustin Pop committed
192 193 194 195 196 197 198 199
  def Rename(self, new_id):
    """Rename this device.

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

    """
    raise NotImplementedError

Iustin Pop's avatar
Iustin Pop committed
200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233
  def Open(self, force=False):
    """Make the device ready for use.

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

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

    """
    raise NotImplementedError

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

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

    """
    raise NotImplementedError

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

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

    """
    result = True
    if self._children:
      for child in self._children:
        result = result and child.SetSyncSpeed(speed)
    return result

234 235 236 237 238 239 240 241 242 243 244 245 246 247
  def PauseResumeSync(self, pause):
    """Pause/Resume the sync of the mirror.

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

    @param pause: Wheater to pause or resume

    """
    result = True
    if self._children:
      for child in self._children:
        result = result and child.PauseResumeSync(pause)
    return result

Iustin Pop's avatar
Iustin Pop committed
248 249 250 251 252 253
  def GetSyncStatus(self):
    """Returns the sync status of the device.

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

254
    If sync_percent is None, it means the device is not syncing.
Iustin Pop's avatar
Iustin Pop committed
255 256

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

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

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

267
    @rtype: objects.BlockDevStatus
Iustin Pop's avatar
Iustin Pop committed
268

Iustin Pop's avatar
Iustin Pop committed
269
    """
270 271 272 273 274 275
    return objects.BlockDevStatus(dev_path=self.dev_path,
                                  major=self.major,
                                  minor=self.minor,
                                  sync_percent=None,
                                  estimated_time=None,
                                  is_degraded=False,
276
                                  ldisk_status=constants.LDS_OKAY)
Iustin Pop's avatar
Iustin Pop committed
277 278 279 280 281 282 283 284

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

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

285 286
    @rtype: objects.BlockDevStatus

Iustin Pop's avatar
Iustin Pop committed
287
    """
288 289 290 291 292
    status = self.GetSyncStatus()

    min_percent = status.sync_percent
    max_time = status.estimated_time
    is_degraded = status.is_degraded
293
    ldisk_status = status.ldisk_status
294

Iustin Pop's avatar
Iustin Pop committed
295 296
    if self._children:
      for child in self._children:
297 298
        child_status = child.GetSyncStatus()

Iustin Pop's avatar
Iustin Pop committed
299
        if min_percent is None:
300 301 302 303
          min_percent = child_status.sync_percent
        elif child_status.sync_percent is not None:
          min_percent = min(min_percent, child_status.sync_percent)

Iustin Pop's avatar
Iustin Pop committed
304
        if max_time is None:
305 306 307 308 309
          max_time = child_status.estimated_time
        elif child_status.estimated_time is not None:
          max_time = max(max_time, child_status.estimated_time)

        is_degraded = is_degraded or child_status.is_degraded
310 311 312 313 314

        if ldisk_status is None:
          ldisk_status = child_status.ldisk_status
        elif child_status.ldisk_status is not None:
          ldisk_status = max(ldisk_status, child_status.ldisk_status)
315 316 317 318 319 320 321

    return objects.BlockDevStatus(dev_path=self.dev_path,
                                  major=self.major,
                                  minor=self.minor,
                                  sync_percent=min_percent,
                                  estimated_time=max_time,
                                  is_degraded=is_degraded,
322
                                  ldisk_status=ldisk_status)
Iustin Pop's avatar
Iustin Pop committed
323

324 325 326 327 328 329 330 331 332
  def SetInfo(self, text):
    """Update metadata with info text.

    Only supported for some device types.

    """
    for child in self._children:
      child.SetInfo(text)

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

336
    @type amount: integer
Iustin Pop's avatar
Iustin Pop committed
337
    @param amount: the amount (in mebibytes) to grow with
338 339 340
    @type dryrun: boolean
    @param dryrun: whether to execute the operation in simulation mode
        only, without actually increasing the size
341 342 343

    """
    raise NotImplementedError
344

345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361
  def GetActualSize(self):
    """Return the actual disk size.

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

    """
    assert self.attached, "BlockDevice not attached in GetActualSize()"
    result = utils.RunCmd(["blockdev", "--getsize64", self.dev_path])
    if result.failed:
      _ThrowError("blockdev failed (%s): %s",
                  result.fail_reason, result.output)
    try:
      sz = int(result.output.strip())
    except (ValueError, TypeError), err:
      _ThrowError("Failed to parse blockdev output: %s", str(err))
    return sz

Iustin Pop's avatar
Iustin Pop committed
362 363 364 365 366 367 368 369 370 371
  def __repr__(self):
    return ("<%s: unique_id: %s, children: %s, %s:%s, %s>" %
            (self.__class__, self.unique_id, self._children,
             self.major, self.minor, self.dev_path))


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

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

Iustin Pop's avatar
Iustin Pop committed
376
  def __init__(self, unique_id, children, size):
Iustin Pop's avatar
Iustin Pop committed
377 378 379 380 381
    """Attaches to a LV device.

    The unique_id is a tuple (vg_name, lv_name)

    """
Iustin Pop's avatar
Iustin Pop committed
382
    super(LogicalVolume, self).__init__(unique_id, children, size)
Iustin Pop's avatar
Iustin Pop committed
383 384 385
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
      raise ValueError("Invalid configuration data %s" % str(unique_id))
    self._vg_name, self._lv_name = unique_id
386 387 388
    self._ValidateName(self._vg_name)
    self._ValidateName(self._lv_name)
    self.dev_path = utils.PathJoin("/dev", self._vg_name, self._lv_name)
Iustin Pop's avatar
Iustin Pop committed
389
    self._degraded = True
390
    self.major = self.minor = self.pe_size = self.stripe_count = None
Iustin Pop's avatar
Iustin Pop committed
391 392 393 394 395 396 397 398
    self.Attach()

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

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

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

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

  @staticmethod
439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472
  def _GetVolumeInfo(lvm_cmd, fields):
    """Returns LVM Volumen infos using lvm_cmd

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

    """
    if not fields:
      raise errors.ProgrammerError("No fields specified")

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

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

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

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

      data.append(splitted_fields)

    return data

  @classmethod
  def GetPVInfo(cls, vg_names, filter_allocatable=True):
Iustin Pop's avatar
Iustin Pop committed
473 474
    """Get the free space info for PVs in a volume group.

475 476
    @param vg_names: list of volume group names, if empty all will be returned
    @param filter_allocatable: whether to skip over unallocatable PVs
Iustin Pop's avatar
Iustin Pop committed
477

Iustin Pop's avatar
Iustin Pop committed
478 479
    @rtype: list
    @return: list of tuples (free_space, name) with free_space in mebibytes
480

Iustin Pop's avatar
Iustin Pop committed
481
    """
482 483 484 485 486
    try:
      info = cls._GetVolumeInfo("pvs", ["pv_name", "vg_name", "pv_free",
                                        "pv_attr"])
    except errors.GenericError, err:
      logging.error("Can't get PV information: %s", err)
Iustin Pop's avatar
Iustin Pop committed
487
      return None
488

Iustin Pop's avatar
Iustin Pop committed
489
    data = []
490
    for pv_name, vg_name, pv_free, pv_attr in info:
491
      # (possibly) skip over pvs which are not allocatable
492
      if filter_allocatable and pv_attr[0] != "a":
Iustin Pop's avatar
Iustin Pop committed
493
        continue
494
      # (possibly) skip over pvs which are not in the right volume group(s)
495
      if vg_names and vg_name not in vg_names:
496
        continue
497 498 499 500 501 502 503 504 505 506 507 508
      data.append((float(pv_free), pv_name, vg_name))

    return data

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

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

    @rtype: list
509 510
    @return: list of tuples (free_space, total_size, name) with free_space in
             MiB
511 512 513

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

    data = []
521
    for vg_name, vg_free, vg_attr, vg_size in info:
522 523 524 525 526 527
      # (possibly) skip over vgs which are not writable
      if filter_readonly and vg_attr[0] == "r":
        continue
      # (possibly) skip over vgs which are not in the right volume group(s)
      if vg_names and vg_name not in vg_names:
        continue
528
      data.append((float(vg_free), float(vg_size), vg_name))
Iustin Pop's avatar
Iustin Pop committed
529 530 531

    return data

532 533 534 535 536 537 538 539 540 541 542
  @classmethod
  def _ValidateName(cls, name):
    """Validates that a given name is valid as VG or LV name.

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

    """
    if (not cls._VALID_NAME_RE.match(name) or
        name in cls._INVALID_NAMES or
543
        compat.any(substring in name for substring in cls._INVALID_SUBSTRINGS)):
544 545
      _ThrowError("Invalid LVM name '%s'", name)

Iustin Pop's avatar
Iustin Pop committed
546 547 548 549 550 551
  def Remove(self):
    """Remove this logical volume.

    """
    if not self.minor and not self.Attach():
      # the LV does not exist
552
      return
Iustin Pop's avatar
Iustin Pop committed
553 554 555
    result = utils.RunCmd(["lvremove", "-f", "%s/%s" %
                           (self._vg_name, self._lv_name)])
    if result.failed:
556
      _ThrowError("Can't lvremove: %s - %s", result.fail_reason, result.output)
Iustin Pop's avatar
Iustin Pop committed
557

Iustin Pop's avatar
Iustin Pop committed
558 559 560 561 562 563 564 565 566 567 568 569 570
  def Rename(self, new_id):
    """Rename this logical volume.

    """
    if not isinstance(new_id, (tuple, list)) or len(new_id) != 2:
      raise errors.ProgrammerError("Invalid new logical id '%s'" % new_id)
    new_vg, new_name = new_id
    if new_vg != self._vg_name:
      raise errors.ProgrammerError("Can't move a logical volume across"
                                   " volume groups (from %s to to %s)" %
                                   (self._vg_name, new_vg))
    result = utils.RunCmd(["lvrename", new_vg, self._lv_name, new_name])
    if result.failed:
571
      _ThrowError("Failed to rename the logical volume: %s", result.output)
572
    self._lv_name = new_name
573
    self.dev_path = utils.PathJoin("/dev", self._vg_name, self._lv_name)
574

Iustin Pop's avatar
Iustin Pop committed
575 576 577 578
  def Attach(self):
    """Attach to an existing LV.

    This method will try to see if an existing and active LV exists
579
    which matches our name. If so, its major/minor will be
Iustin Pop's avatar
Iustin Pop committed
580 581 582
    recorded.

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

608
    status, major, minor, pe_size, stripes = out
Iustin Pop's avatar
Iustin Pop committed
609
    if len(status) != 6:
610
      logging.error("lvs lv_attr is not 6 characters (%s)", status)
Iustin Pop's avatar
Iustin Pop committed
611 612 613 614 615
      return False

    try:
      major = int(major)
      minor = int(minor)
616
    except (TypeError, ValueError), err:
617
      logging.error("lvs major/minor cannot be parsed: %s", str(err))
Iustin Pop's avatar
Iustin Pop committed
618

619 620 621 622 623 624 625 626 627 628 629 630
    try:
      pe_size = int(float(pe_size))
    except (TypeError, ValueError), err:
      logging.error("Can't parse vg extent size: %s", err)
      return False

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

Iustin Pop's avatar
Iustin Pop committed
631 632
    self.major = major
    self.minor = minor
633 634
    self.pe_size = pe_size
    self.stripe_count = stripes
Iustin Pop's avatar
Iustin Pop committed
635
    self._degraded = status[0] == "v" # virtual volume, i.e. doesn't backing
Iustin Pop's avatar
Iustin Pop committed
636
                                      # storage
637
    self.attached = True
Iustin Pop's avatar
Iustin Pop committed
638
    return True
Iustin Pop's avatar
Iustin Pop committed
639 640 641 642

  def Assemble(self):
    """Assemble the device.

Michael Hanselmann's avatar
Michael Hanselmann committed
643
    We always run `lvchange -ay` on the LV to ensure it's active before
644 645
    use, as there were cases when xenvg was not active after boot
    (also possibly after disk issues).
Iustin Pop's avatar
Iustin Pop committed
646 647

    """
648 649
    result = utils.RunCmd(["lvchange", "-ay", self.dev_path])
    if result.failed:
650
      _ThrowError("Can't activate lv %s: %s", self.dev_path, result.output)
Iustin Pop's avatar
Iustin Pop committed
651 652 653 654 655 656 657 658

  def Shutdown(self):
    """Shutdown the device.

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

    """
659
    pass
Iustin Pop's avatar
Iustin Pop committed
660

661 662 663 664 665 666 667 668
  def GetSyncStatus(self):
    """Returns the sync status of the device.

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

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

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

Iustin Pop's avatar
Iustin Pop committed
678 679
    The status was already read in Attach, so we just return it.

680
    @rtype: objects.BlockDevStatus
Iustin Pop's avatar
Iustin Pop committed
681

682
    """
683 684 685 686 687
    if self._degraded:
      ldisk_status = constants.LDS_FAULTY
    else:
      ldisk_status = constants.LDS_OKAY

688 689 690 691 692 693
    return objects.BlockDevStatus(dev_path=self.dev_path,
                                  major=self.major,
                                  minor=self.minor,
                                  sync_percent=None,
                                  estimated_time=None,
                                  is_degraded=self._degraded,
694
                                  ldisk_status=ldisk_status)
695

Iustin Pop's avatar
Iustin Pop committed
696 697 698 699 700 701
  def Open(self, force=False):
    """Make the device ready for I/O.

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

    """
702
    pass
Iustin Pop's avatar
Iustin Pop committed
703 704 705 706 707 708 709

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

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

    """
710
    pass
Iustin Pop's avatar
Iustin Pop committed
711 712 713 714

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

Iustin Pop's avatar
Iustin Pop committed
715 716
    @returns: tuple (vg, lv)

Iustin Pop's avatar
Iustin Pop committed
717 718 719 720
    """
    snap_name = self._lv_name + ".snap"

    # remove existing snapshot if found
Iustin Pop's avatar
Iustin Pop committed
721
    snap = LogicalVolume((self._vg_name, snap_name), None, size)
722
    _IgnoreError(snap.Remove)
Iustin Pop's avatar
Iustin Pop committed
723

724 725 726
    vg_info = self.GetVGInfo([self._vg_name])
    if not vg_info:
      _ThrowError("Can't compute VG info for vg %s", self._vg_name)
727
    free_size, _, _ = vg_info[0]
Iustin Pop's avatar
Iustin Pop committed
728
    if free_size < size:
729 730
      _ThrowError("Not enough free space: required %s,"
                  " available %s", size, free_size)
Iustin Pop's avatar
Iustin Pop committed
731 732 733 734

    result = utils.RunCmd(["lvcreate", "-L%dm" % size, "-s",
                           "-n%s" % snap_name, self.dev_path])
    if result.failed:
735 736
      _ThrowError("command: %s error: %s - %s",
                  result.cmd, result.fail_reason, result.output)
Iustin Pop's avatar
Iustin Pop committed
737

Iustin Pop's avatar
Iustin Pop committed
738
    return (self._vg_name, snap_name)
Iustin Pop's avatar
Iustin Pop committed
739

740 741 742 743 744 745 746
  def SetInfo(self, text):
    """Update metadata with info text.

    """
    BlockDev.SetInfo(self, text)

    # Replace invalid characters
Iustin Pop's avatar
Iustin Pop committed
747 748
    text = re.sub("^[^A-Za-z0-9_+.]", "_", text)
    text = re.sub("[^-A-Za-z0-9_+.]", "_", text)
749 750 751 752 753 754 755

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

    result = utils.RunCmd(["lvchange", "--addtag", text,
                           self.dev_path])
    if result.failed:
756 757 758
      _ThrowError("Command: %s error: %s - %s", result.cmd, result.fail_reason,
                  result.output)

759
  def Grow(self, amount, dryrun):
760 761 762
    """Grow the logical volume.

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


784 785 786 787 788 789
class DRBD8Status(object):
  """A DRBD status representation class.

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

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

799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839
  CS_UNCONFIGURED = "Unconfigured"
  CS_STANDALONE = "StandAlone"
  CS_WFCONNECTION = "WFConnection"
  CS_WFREPORTPARAMS = "WFReportParams"
  CS_CONNECTED = "Connected"
  CS_STARTINGSYNCS = "StartingSyncS"
  CS_STARTINGSYNCT = "StartingSyncT"
  CS_WFBITMAPS = "WFBitMapS"
  CS_WFBITMAPT = "WFBitMapT"
  CS_WFSYNCUUID = "WFSyncUUID"
  CS_SYNCSOURCE = "SyncSource"
  CS_SYNCTARGET = "SyncTarget"
  CS_PAUSEDSYNCS = "PausedSyncS"
  CS_PAUSEDSYNCT = "PausedSyncT"
  CSET_SYNC = frozenset([
    CS_WFREPORTPARAMS,
    CS_STARTINGSYNCS,
    CS_STARTINGSYNCT,
    CS_WFBITMAPS,
    CS_WFBITMAPT,
    CS_WFSYNCUUID,
    CS_SYNCSOURCE,
    CS_SYNCTARGET,
    CS_PAUSEDSYNCS,
    CS_PAUSEDSYNCT,
    ])

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

  RO_PRIMARY = "Primary"
  RO_SECONDARY = "Secondary"
  RO_UNKNOWN = "Unknown"

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

    # end reading of data from the LINE_RE or UNCONF_RE
856

857 858 859 860 861 862 863
    self.is_standalone = self.cstatus == self.CS_STANDALONE
    self.is_wfconn = self.cstatus == self.CS_WFCONNECTION
    self.is_connected = self.cstatus == self.CS_CONNECTED
    self.is_primary = self.lrole == self.RO_PRIMARY
    self.is_secondary = self.lrole == self.RO_SECONDARY
    self.peer_primary = self.rrole == self.RO_PRIMARY
    self.peer_secondary = self.rrole == self.RO_SECONDARY
864 865 866
    self.both_primary = self.is_primary and self.peer_primary
    self.both_secondary = self.is_secondary and self.peer_secondary

867 868
    self.is_diskless = self.ldisk == self.DS_DISKLESS
    self.is_disk_uptodate = self.ldisk == self.DS_UPTODATE
869

870 871
    self.is_in_resync = self.cstatus in self.CSET_SYNC
    self.is_in_use = self.cstatus != self.CS_UNCONFIGURED
Iustin Pop's avatar
Iustin Pop committed
872

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


892
class BaseDRBD(BlockDev): # pylint: disable=W0223
893
  """Base DRBD class.
Iustin Pop's avatar
Iustin Pop committed
894

895 896 897
  This class contains a few bits of common functionality between the
  0.7 and 8.x versions of DRBD.

898
  """
899
  _VERSION_RE = re.compile(r"^version: (\d+)\.(\d+)\.(\d+)(?:\.\d+)?"
900
                           r" \(api:(\d+)/proto:(\d+)(?:-(\d+))?\)")
901 902
  _VALID_LINE_RE = re.compile("^ *([0-9]+): cs:([^ ]+).*$")
  _UNUSED_LINE_RE = re.compile("^ *([0-9]+): cs:Unconfigured$")
Iustin Pop's avatar
Iustin Pop committed
903

904 905 906 907
  _DRBD_MAJOR = 147
  _ST_UNCONFIGURED = "Unconfigured"
  _ST_WFCONNECTION = "WFConnection"
  _ST_CONNECTED = "Connected"
Iustin Pop's avatar
Iustin Pop committed
908

909
  _STATUS_FILE = "/proc/drbd"
910
  _USERMODE_HELPER_FILE = "/sys/module/drbd/parameters/usermode_helper"
911

912
  @staticmethod
913
  def _GetProcData(filename=_STATUS_FILE):
914
    """Return data from /proc/drbd.
Iustin Pop's avatar
Iustin Pop committed
915 916

    """
917
    try:
918
      data = utils.ReadFile(filename).splitlines()
919 920 921 922 923 924
    except EnvironmentError, err:
      if err.errno == errno.ENOENT:
        _ThrowError("The file %s cannot be opened, check if the module"
                    " is loaded (%s)", filename, str(err))
      else:
        _ThrowError("Can't read the DRBD proc file %s: %s", filename, str(err))
925
    if not data:
926
      _ThrowError("Can't read any data from %s", filename)
927
    return data
Iustin Pop's avatar
Iustin Pop committed
928

929 930
  @classmethod
  def _MassageProcData(cls, data):
931
    """Transform the output of _GetProdData into a nicer form.
Iustin Pop's avatar
Iustin Pop committed
932

Iustin Pop's avatar
Iustin Pop committed
933 934
    @return: a dictionary of minor: joined lines from /proc/drbd
        for that minor
Iustin Pop's avatar
Iustin Pop committed
935 936

    """
937 938 939
    results = {}
    old_minor = old_line = None
    for line in data:
940 941
      if not line: # completely empty lines, as can be returned by drbd8.0+
        continue
942
      lresult = cls._VALID_LINE_RE.match(line)
943 944 945 946 947 948 949 950 951 952 953 954
      if lresult is not None:
        if old_minor is not None:
          results[old_minor] = old_line
        old_minor = int(lresult.group(1))
        old_line = line
      else:
        if old_minor is not None:
          old_line += " " + line.strip()
    # add last line
    if old_minor is not None:
      results[old_minor] = old_line
    return results
Iustin Pop's avatar
Iustin Pop committed
955

956
  @classmethod
957
  def _GetVersion(cls, proc_data):
958
    """Return the DRBD version.
Iustin Pop's avatar
Iustin Pop committed
959

960
    This will return a dict with keys:
Iustin Pop's avatar
Iustin Pop committed
961 962 963 964 965 966
      - k_major
      - k_minor
      - k_point
      - api
      - proto
      - proto2 (only on drbd > 8.2.X)
Iustin Pop's avatar
Iustin Pop committed
967 968

    """
969 970 971 972 973
    first_line = proc_data[0].strip()
    version = cls._VERSION_RE.match(first_line)
    if not version:
      raise errors.BlockDeviceError("Can't parse DRBD version from '%s'" %
                                    first_line)
Iustin Pop's avatar
Iustin Pop committed
974

975
    values = version.groups()
Iustin Pop's avatar
Iustin Pop committed
976 977 978 979 980
    retval = {"k_major": int(values[0]),
              "k_minor": int(values[1]),
              "k_point": int(values[2]),
              "api": int(values[3]),
              "proto": int(values[4]),
981 982
             }
    if values[5] is not None:
Iustin Pop's avatar
Iustin Pop committed
983
      retval["proto2"] = values[5]
Iustin Pop's avatar
Iustin Pop committed
984

985 986
    return retval

987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003
  @staticmethod
  def GetUsermodeHelper(filename=_USERMODE_HELPER_FILE):
    """Returns DRBD usermode_helper currently set.

    """
    try:
      helper = utils.ReadFile(filename).splitlines()[0]
    except EnvironmentError, err:
      if err.errno == errno.ENOENT:
        _ThrowError("The file %s cannot be opened, check if the module"
                    " is loaded (%s)", filename, str(err))
      else:
        _ThrowError("Can't read DRBD helper file %s: %s", filename, str(err))
    if not helper:
      _ThrowError("Can't read any data from %s", filename)
    return helper

1004 1005 1006
  @staticmethod
  def _DevPath(minor):
    """Return the path to a drbd device for a given minor.
Iustin Pop's avatar
Iustin Pop committed
1007 1008

    """
1009
    return "/dev/drbd%d" % minor
Iustin Pop's avatar
Iustin Pop committed
1010

1011
  @classmethod
1012
  def GetUsedDevs(cls):
1013
    """Compute the list of used DRBD devices.
Iustin Pop's avatar
Iustin Pop committed
1014 1015

    """
1016
    data = cls._GetProcData()
Iustin Pop's avatar
Iustin Pop committed
1017

1018 1019
    used_devs = {}
    for line in data:
1020
      match = cls._VALID_LINE_RE.match(line)
1021 1022 1023 1024 1025 1026 1027
      if not match:
        continue
      minor = int(match.group(1))
      state = match.group(2)
      if state == cls._ST_UNCONFIGURED:
        continue
      used_devs[minor] = state, line
Iustin Pop's avatar
Iustin Pop committed
1028

1029
    return used_devs
Iustin Pop's avatar
Iustin Pop committed
1030

1031 1032
  def _SetFromMinor(self, minor):
    """Set our parameters based on the given minor.
1033

1034
    This sets our minor variable and our dev_path.
Iustin Pop's avatar
Iustin Pop committed
1035 1036

    """
1037 1038
    if minor is None:
      self.minor = self.dev_path = None
1039
      self.attached = False
Iustin Pop's avatar
Iustin Pop committed
1040
    else:
1041 1042
      self.minor = minor
      self.dev_path = self._DevPath(minor)
1043
      self.attached = True
Iustin Pop's avatar
Iustin Pop committed
1044 1045

  @staticmethod
1046 1047
  def _CheckMetaSize(meta_device):
    """Check if the given meta device looks like a valid one.
Iustin Pop's avatar
Iustin Pop committed
1048

1049 1050
    This currently only check the size, which must be around
    128MiB.
Iustin Pop's avatar
Iustin Pop committed
1051 1052

    """
1053 1054
    result = utils.RunCmd(["blockdev", "--getsize", meta_device])
    if result.failed:
Iustin Pop's avatar
Iustin Pop committed
1055 1056
      _ThrowError("Failed to get device size: %s - %s",
                  result.fail_reason, result.output)
Iustin Pop's avatar
Iustin Pop committed
1057
    try:
1058
      sectors = int(result.stdout)
1059
    except (TypeError, ValueError):
Iustin Pop's avatar
Iustin Pop committed
1060
      _ThrowError("Invalid output from blockdev: '%s'", result.stdout)
Iustin Pop's avatar
Iustin Pop committed
1061 1062 1063
    num_bytes = sectors * 512
    if num_bytes < 128 * 1024 * 1024: # less than 128MiB
      _ThrowError("Meta device too small (%.2fMib)", (num_bytes / 1024 / 1024))
1064 1065 1066 1067