Statistics
| Branch: | Tag: | Revision:

root / lib / bdev.py @ 5ae4945a

History | View | Annotate | Download (87 kB)

1
#
2
#
3

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

    
21

    
22
"""Block device abstraction"""
23

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

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

    
40

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

    
44

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

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

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

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

    
62

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

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

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

    
76

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

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

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

    
90

    
91
class BlockDev(object):
92
  """Block device abstract class.
93

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

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

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

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

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

122
  You can get to a device in two ways:
123
    - creating the (real) device, which returns you
124
      an attached instance (lvcreate)
125
    - attaching of a python instance to an existing (real) device
126

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

133
  """
134
  def __init__(self, unique_id, children, size, params):
135
    self._children = children
136
    self.dev_path = None
137
    self.unique_id = unique_id
138
    self.major = None
139
    self.minor = None
140
    self.attached = False
141
    self.size = size
142
    self.params = params
143

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

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

155
    """
156
    pass
157

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

161
    """
162
    raise NotImplementedError
163

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

167
    """
168
    raise NotImplementedError
169

    
170
  @classmethod
171
  def Create(cls, unique_id, children, size, params):
172
    """Create the device.
173

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

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

181
    """
182
    raise NotImplementedError
183

    
184
  def Remove(self):
185
    """Remove this device.
186

187
    This makes sense only for some of the device types: LV and file
188
    storage. Also note that if the device can't attach, the removal
189
    can't be completed.
190

191
    """
192
    raise NotImplementedError
193

    
194
  def Rename(self, new_id):
195
    """Rename this device.
196

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

199
    """
200
    raise NotImplementedError
201

    
202
  def Open(self, force=False):
203
    """Make the device ready for use.
204

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

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

211
    """
212
    raise NotImplementedError
213

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

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

221
    """
222
    raise NotImplementedError
223

    
224
  def SetSyncParams(self, params):
225
    """Adjust the synchronization parameters of the mirror.
226

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

229
    @param params: dictionary of LD level disk parameters related to the
230
    synchronization.
231
    @rtype: list
232
    @return: a list of error messages, emitted both by the current node and by
233
    children. An empty list means no errors.
234

235
    """
236
    result = []
237
    if self._children:
238
      for child in self._children:
239
        result.extend(child.SetSyncParams(params))
240
    return result
241

    
242
  def PauseResumeSync(self, pause):
243
    """Pause/Resume the sync of the mirror.
244

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

247
    @param pause: Whether to pause or resume
248

249
    """
250
    result = True
251
    if self._children:
252
      for child in self._children:
253
        result = result and child.PauseResumeSync(pause)
254
    return result
255

    
256
  def GetSyncStatus(self):
257
    """Returns the sync status of the device.
258

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

262
    If sync_percent is None, it means the device is not syncing.
263

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

267
    If is_degraded is True, it means the device is missing
268
    redundancy. This is usually a sign that something went wrong in
269
    the device setup, if sync_percent is None.
270

271
    The ldisk parameter represents the degradation of the local
272
    data. This is only valid for some devices, the rest will always
273
    return False (not degraded).
274

275
    @rtype: objects.BlockDevStatus
276

277
    """
278
    return objects.BlockDevStatus(dev_path=self.dev_path,
279
                                  major=self.major,
280
                                  minor=self.minor,
281
                                  sync_percent=None,
282
                                  estimated_time=None,
283
                                  is_degraded=False,
284
                                  ldisk_status=constants.LDS_OKAY)
285

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

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

293
    @rtype: objects.BlockDevStatus
294

295
    """
296
    status = self.GetSyncStatus()
297

    
298
    min_percent = status.sync_percent
299
    max_time = status.estimated_time
300
    is_degraded = status.is_degraded
301
    ldisk_status = status.ldisk_status
302

    
303
    if self._children:
304
      for child in self._children:
305
        child_status = child.GetSyncStatus()
306

    
307
        if min_percent is None:
308
          min_percent = child_status.sync_percent
309
        elif child_status.sync_percent is not None:
310
          min_percent = min(min_percent, child_status.sync_percent)
311

    
312
        if max_time is None:
313
          max_time = child_status.estimated_time
314
        elif child_status.estimated_time is not None:
315
          max_time = max(max_time, child_status.estimated_time)
316

    
317
        is_degraded = is_degraded or child_status.is_degraded
318

    
319
        if ldisk_status is None:
320
          ldisk_status = child_status.ldisk_status
321
        elif child_status.ldisk_status is not None:
322
          ldisk_status = max(ldisk_status, child_status.ldisk_status)
323

    
324
    return objects.BlockDevStatus(dev_path=self.dev_path,
325
                                  major=self.major,
326
                                  minor=self.minor,
327
                                  sync_percent=min_percent,
328
                                  estimated_time=max_time,
329
                                  is_degraded=is_degraded,
330
                                  ldisk_status=ldisk_status)
331

    
332
  def SetInfo(self, text):
333
    """Update metadata with info text.
334

335
    Only supported for some device types.
336

337
    """
338
    for child in self._children:
339
      child.SetInfo(text)
340

    
341
  def Grow(self, amount, dryrun, backingstore):
342
    """Grow the block device.
343

344
    @type amount: integer
345
    @param amount: the amount (in mebibytes) to grow with
346
    @type dryrun: boolean
347
    @param dryrun: whether to execute the operation in simulation mode
348
        only, without actually increasing the size
349
    @param backingstore: whether to execute the operation on backing storage
350
        only, or on "logical" storage only; e.g. DRBD is logical storage,
351
        whereas LVM, file, RBD are backing storage
352

353
    """
354
    raise NotImplementedError
355

    
356
  def GetActualSize(self):
357
    """Return the actual disk size.
358

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

361
    """
362
    assert self.attached, "BlockDevice not attached in GetActualSize()"
363
    result = utils.RunCmd(["blockdev", "--getsize64", self.dev_path])
364
    if result.failed:
365
      _ThrowError("blockdev failed (%s): %s",
366
                  result.fail_reason, result.output)
367
    try:
368
      sz = int(result.output.strip())
369
    except (ValueError, TypeError), err:
370
      _ThrowError("Failed to parse blockdev output: %s", str(err))
371
    return sz
372

    
373
  def __repr__(self):
374
    return ("<%s: unique_id: %s, children: %s, %s:%s, %s>" %
375
            (self.__class__, self.unique_id, self._children,
376
             self.major, self.minor, self.dev_path))
377

    
378

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

382
  """
383
  _VALID_NAME_RE = re.compile("^[a-zA-Z0-9+_.-]*$")
384
  _INVALID_NAMES = frozenset([".", "..", "snapshot", "pvmove"])
385
  _INVALID_SUBSTRINGS = frozenset(["_mlog", "_mimage"])
386

    
387
  def __init__(self, unique_id, children, size, params):
388
    """Attaches to a LV device.
389

390
    The unique_id is a tuple (vg_name, lv_name)
391

392
    """
393
    super(LogicalVolume, self).__init__(unique_id, children, size, params)
394
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
395
      raise ValueError("Invalid configuration data %s" % str(unique_id))
396
    self._vg_name, self._lv_name = unique_id
397
    self._ValidateName(self._vg_name)
398
    self._ValidateName(self._lv_name)
399
    self.dev_path = utils.PathJoin("/dev", self._vg_name, self._lv_name)
400
    self._degraded = True
401
    self.major = self.minor = self.pe_size = self.stripe_count = None
402
    self.Attach()
403

    
404
  @classmethod
405
  def Create(cls, unique_id, children, size, params):
406
    """Create a new logical volume.
407

408
    """
409
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
410
      raise errors.ProgrammerError("Invalid configuration data %s" %
411
                                   str(unique_id))
412
    vg_name, lv_name = unique_id
413
    cls._ValidateName(vg_name)
414
    cls._ValidateName(lv_name)
415
    pvs_info = cls.GetPVInfo([vg_name])
416
    if not pvs_info:
417
      _ThrowError("Can't compute PV info for vg %s", vg_name)
418
    pvs_info.sort()
419
    pvs_info.reverse()
420

    
421
    pvlist = [pv[1] for pv in pvs_info]
422
    if compat.any(":" in v for v in pvlist):
423
      _ThrowError("Some of your PVs have the invalid character ':' in their"
424
                  " name, this is not supported - please filter them out"
425
                  " in lvm.conf using either 'filter' or 'preferred_names'")
426
    free_size = sum([pv[0] for pv in pvs_info])
427
    current_pvs = len(pvlist)
428
    desired_stripes = params[constants.LDP_STRIPES]
429
    stripes = min(current_pvs, desired_stripes)
430
    if stripes < desired_stripes:
431
      logging.warning("Could not use %d stripes for VG %s, as only %d PVs are"
432
                      " available.", desired_stripes, vg_name, current_pvs)
433

    
434
    # The size constraint should have been checked from the master before
435
    # calling the create function.
436
    if free_size < size:
437
      _ThrowError("Not enough free space: required %s,"
438
                  " available %s", size, free_size)
439
    cmd = ["lvcreate", "-L%dm" % size, "-n%s" % lv_name]
440
    # If the free space is not well distributed, we won't be able to
441
    # create an optimally-striped volume; in that case, we want to try
442
    # with N, N-1, ..., 2, and finally 1 (non-stripped) number of
443
    # stripes
444
    for stripes_arg in range(stripes, 0, -1):
445
      result = utils.RunCmd(cmd + ["-i%d" % stripes_arg] + [vg_name] + pvlist)
446
      if not result.failed:
447
        break
448
    if result.failed:
449
      _ThrowError("LV create failed (%s): %s",
450
                  result.fail_reason, result.output)
451
    return LogicalVolume(unique_id, children, size, params)
452

    
453
  @staticmethod
454
  def _GetVolumeInfo(lvm_cmd, fields):
455
    """Returns LVM Volumen infos using lvm_cmd
456

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

461
    """
462
    if not fields:
463
      raise errors.ProgrammerError("No fields specified")
464

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

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

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

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

    
482
      data.append(splitted_fields)
483

    
484
    return data
485

    
486
  @classmethod
487
  def GetPVInfo(cls, vg_names, filter_allocatable=True):
488
    """Get the free space info for PVs in a volume group.
489

490
    @param vg_names: list of volume group names, if empty all will be returned
491
    @param filter_allocatable: whether to skip over unallocatable PVs
492

493
    @rtype: list
494
    @return: list of tuples (free_space, name) with free_space in mebibytes
495

496
    """
497
    try:
498
      info = cls._GetVolumeInfo("pvs", ["pv_name", "vg_name", "pv_free",
499
                                        "pv_attr"])
500
    except errors.GenericError, err:
501
      logging.error("Can't get PV information: %s", err)
502
      return None
503

    
504
    data = []
505
    for pv_name, vg_name, pv_free, pv_attr in info:
506
      # (possibly) skip over pvs which are not allocatable
507
      if filter_allocatable and pv_attr[0] != "a":
508
        continue
509
      # (possibly) skip over pvs which are not in the right volume group(s)
510
      if vg_names and vg_name not in vg_names:
511
        continue
512
      data.append((float(pv_free), pv_name, vg_name))
513

    
514
    return data
515

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

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

523
    @rtype: list
524
    @return: list of tuples (free_space, total_size, name) with free_space in
525
             MiB
526

527
    """
528
    try:
529
      info = cls._GetVolumeInfo("vgs", ["vg_name", "vg_free", "vg_attr",
530
                                        "vg_size"])
531
    except errors.GenericError, err:
532
      logging.error("Can't get VG information: %s", err)
533
      return None
534

    
535
    data = []
536
    for vg_name, vg_free, vg_attr, vg_size in info:
537
      # (possibly) skip over vgs which are not writable
538
      if filter_readonly and vg_attr[0] == "r":
539
        continue
540
      # (possibly) skip over vgs which are not in the right volume group(s)
541
      if vg_names and vg_name not in vg_names:
542
        continue
543
      data.append((float(vg_free), float(vg_size), vg_name))
544

    
545
    return data
546

    
547
  @classmethod
548
  def _ValidateName(cls, name):
549
    """Validates that a given name is valid as VG or LV name.
550

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

555
    """
556
    if (not cls._VALID_NAME_RE.match(name) or
557
        name in cls._INVALID_NAMES or
558
        compat.any(substring in name for substring in cls._INVALID_SUBSTRINGS)):
559
      _ThrowError("Invalid LVM name '%s'", name)
560

    
561
  def Remove(self):
562
    """Remove this logical volume.
563

564
    """
565
    if not self.minor and not self.Attach():
566
      # the LV does not exist
567
      return
568
    result = utils.RunCmd(["lvremove", "-f", "%s/%s" %
569
                           (self._vg_name, self._lv_name)])
570
    if result.failed:
571
      _ThrowError("Can't lvremove: %s - %s", result.fail_reason, result.output)
572

    
573
  def Rename(self, new_id):
574
    """Rename this logical volume.
575

576
    """
577
    if not isinstance(new_id, (tuple, list)) or len(new_id) != 2:
578
      raise errors.ProgrammerError("Invalid new logical id '%s'" % new_id)
579
    new_vg, new_name = new_id
580
    if new_vg != self._vg_name:
581
      raise errors.ProgrammerError("Can't move a logical volume across"
582
                                   " volume groups (from %s to to %s)" %
583
                                   (self._vg_name, new_vg))
584
    result = utils.RunCmd(["lvrename", new_vg, self._lv_name, new_name])
585
    if result.failed:
586
      _ThrowError("Failed to rename the logical volume: %s", result.output)
587
    self._lv_name = new_name
588
    self.dev_path = utils.PathJoin("/dev", self._vg_name, self._lv_name)
589

    
590
  def Attach(self):
591
    """Attach to an existing LV.
592

593
    This method will try to see if an existing and active LV exists
594
    which matches our name. If so, its major/minor will be
595
    recorded.
596

597
    """
598
    self.attached = False
599
    result = utils.RunCmd(["lvs", "--noheadings", "--separator=,",
600
                           "--units=m", "--nosuffix",
601
                           "-olv_attr,lv_kernel_major,lv_kernel_minor,"
602
                           "vg_extent_size,stripes", self.dev_path])
603
    if result.failed:
604
      logging.error("Can't find LV %s: %s, %s",
605
                    self.dev_path, result.fail_reason, result.output)
606
      return False
607
    # the output can (and will) have multiple lines for multi-segment
608
    # LVs, as the 'stripes' parameter is a segment one, so we take
609
    # only the last entry, which is the one we're interested in; note
610
    # that with LVM2 anyway the 'stripes' value must be constant
611
    # across segments, so this is a no-op actually
612
    out = result.stdout.splitlines()
613
    if not out: # totally empty result? splitlines() returns at least
614
                # one line for any non-empty string
615
      logging.error("Can't parse LVS output, no lines? Got '%s'", str(out))
616
      return False
617
    out = out[-1].strip().rstrip(",")
618
    out = out.split(",")
619
    if len(out) != 5:
620
      logging.error("Can't parse LVS output, len(%s) != 5", str(out))
621
      return False
622

    
623
    status, major, minor, pe_size, stripes = out
624
    if len(status) < 6:
625
      logging.error("lvs lv_attr is not at least 6 characters (%s)", status)
626
      return False
627

    
628
    try:
629
      major = int(major)
630
      minor = int(minor)
631
    except (TypeError, ValueError), err:
632
      logging.error("lvs major/minor cannot be parsed: %s", str(err))
633

    
634
    try:
635
      pe_size = int(float(pe_size))
636
    except (TypeError, ValueError), err:
637
      logging.error("Can't parse vg extent size: %s", err)
638
      return False
639

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

    
646
    self.major = major
647
    self.minor = minor
648
    self.pe_size = pe_size
649
    self.stripe_count = stripes
650
    self._degraded = status[0] == "v" # virtual volume, i.e. doesn't backing
651
                                      # storage
652
    self.attached = True
653
    return True
654

    
655
  def Assemble(self):
656
    """Assemble the device.
657

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

662
    """
663
    result = utils.RunCmd(["lvchange", "-ay", self.dev_path])
664
    if result.failed:
665
      _ThrowError("Can't activate lv %s: %s", self.dev_path, result.output)
666

    
667
  def Shutdown(self):
668
    """Shutdown the device.
669

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

673
    """
674
    pass
675

    
676
  def GetSyncStatus(self):
677
    """Returns the sync status of the device.
678

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

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

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

693
    The status was already read in Attach, so we just return it.
694

695
    @rtype: objects.BlockDevStatus
696

697
    """
698
    if self._degraded:
699
      ldisk_status = constants.LDS_FAULTY
700
    else:
701
      ldisk_status = constants.LDS_OKAY
702

    
703
    return objects.BlockDevStatus(dev_path=self.dev_path,
704
                                  major=self.major,
705
                                  minor=self.minor,
706
                                  sync_percent=None,
707
                                  estimated_time=None,
708
                                  is_degraded=self._degraded,
709
                                  ldisk_status=ldisk_status)
710

    
711
  def Open(self, force=False):
712
    """Make the device ready for I/O.
713

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

716
    """
717
    pass
718

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

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

724
    """
725
    pass
726

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

730
    @returns: tuple (vg, lv)
731

732
    """
733
    snap_name = self._lv_name + ".snap"
734

    
735
    # remove existing snapshot if found
736
    snap = LogicalVolume((self._vg_name, snap_name), None, size, self.params)
737
    _IgnoreError(snap.Remove)
738

    
739
    vg_info = self.GetVGInfo([self._vg_name])
740
    if not vg_info:
741
      _ThrowError("Can't compute VG info for vg %s", self._vg_name)
742
    free_size, _, _ = vg_info[0]
743
    if free_size < size:
744
      _ThrowError("Not enough free space: required %s,"
745
                  " available %s", size, free_size)
746

    
747
    result = utils.RunCmd(["lvcreate", "-L%dm" % size, "-s",
748
                           "-n%s" % snap_name, self.dev_path])
749
    if result.failed:
750
      _ThrowError("command: %s error: %s - %s",
751
                  result.cmd, result.fail_reason, result.output)
752

    
753
    return (self._vg_name, snap_name)
754

    
755
  def SetInfo(self, text):
756
    """Update metadata with info text.
757

758
    """
759
    BlockDev.SetInfo(self, text)
760

    
761
    # Replace invalid characters
762
    text = re.sub("^[^A-Za-z0-9_+.]", "_", text)
763
    text = re.sub("[^-A-Za-z0-9_+.]", "_", text)
764

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

    
768
    result = utils.RunCmd(["lvchange", "--addtag", text,
769
                           self.dev_path])
770
    if result.failed:
771
      _ThrowError("Command: %s error: %s - %s", result.cmd, result.fail_reason,
772
                  result.output)
773

    
774
  def Grow(self, amount, dryrun, backingstore):
775
    """Grow the logical volume.
776

777
    """
778
    if not backingstore:
779
      return
780
    if self.pe_size is None or self.stripe_count is None:
781
      if not self.Attach():
782
        _ThrowError("Can't attach to LV during Grow()")
783
    full_stripe_size = self.pe_size * self.stripe_count
784
    rest = amount % full_stripe_size
785
    if rest != 0:
786
      amount += full_stripe_size - rest
787
    cmd = ["lvextend", "-L", "+%dm" % amount]
788
    if dryrun:
789
      cmd.append("--test")
790
    # we try multiple algorithms since the 'best' ones might not have
791
    # space available in the right place, but later ones might (since
792
    # they have less constraints); also note that only recent LVM
793
    # supports 'cling'
794
    for alloc_policy in "contiguous", "cling", "normal":
795
      result = utils.RunCmd(cmd + ["--alloc", alloc_policy, self.dev_path])
796
      if not result.failed:
797
        return
798
    _ThrowError("Can't grow LV %s: %s", self.dev_path, result.output)
799

    
800

    
801
class DRBD8Status(object):
802
  """A DRBD status representation class.
803

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

806
  """
807
  UNCONF_RE = re.compile(r"\s*[0-9]+:\s*cs:Unconfigured$")
808
  LINE_RE = re.compile(r"\s*[0-9]+:\s*cs:(\S+)\s+(?:st|ro):([^/]+)/(\S+)"
809
                       "\s+ds:([^/]+)/(\S+)\s+.*$")
810
  SYNC_RE = re.compile(r"^.*\ssync'ed:\s*([0-9.]+)%.*"
811
                       # Due to a bug in drbd in the kernel, introduced in
812
                       # commit 4b0715f096 (still unfixed as of 2011-08-22)
813
                       "(?:\s|M)"
814
                       "finish: ([0-9]+):([0-9]+):([0-9]+)\s.*$")
815

    
816
  CS_UNCONFIGURED = "Unconfigured"
817
  CS_STANDALONE = "StandAlone"
818
  CS_WFCONNECTION = "WFConnection"
819
  CS_WFREPORTPARAMS = "WFReportParams"
820
  CS_CONNECTED = "Connected"
821
  CS_STARTINGSYNCS = "StartingSyncS"
822
  CS_STARTINGSYNCT = "StartingSyncT"
823
  CS_WFBITMAPS = "WFBitMapS"
824
  CS_WFBITMAPT = "WFBitMapT"
825
  CS_WFSYNCUUID = "WFSyncUUID"
826
  CS_SYNCSOURCE = "SyncSource"
827
  CS_SYNCTARGET = "SyncTarget"
828
  CS_PAUSEDSYNCS = "PausedSyncS"
829
  CS_PAUSEDSYNCT = "PausedSyncT"
830
  CSET_SYNC = frozenset([
831
    CS_WFREPORTPARAMS,
832
    CS_STARTINGSYNCS,
833
    CS_STARTINGSYNCT,
834
    CS_WFBITMAPS,
835
    CS_WFBITMAPT,
836
    CS_WFSYNCUUID,
837
    CS_SYNCSOURCE,
838
    CS_SYNCTARGET,
839
    CS_PAUSEDSYNCS,
840
    CS_PAUSEDSYNCT,
841
    ])
842

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

    
853
  RO_PRIMARY = "Primary"
854
  RO_SECONDARY = "Secondary"
855
  RO_UNKNOWN = "Unknown"
856

    
857
  def __init__(self, procline):
858
    u = self.UNCONF_RE.match(procline)
859
    if u:
860
      self.cstatus = self.CS_UNCONFIGURED
861
      self.lrole = self.rrole = self.ldisk = self.rdisk = None
862
    else:
863
      m = self.LINE_RE.match(procline)
864
      if not m:
865
        raise errors.BlockDeviceError("Can't parse input data '%s'" % procline)
866
      self.cstatus = m.group(1)
867
      self.lrole = m.group(2)
868
      self.rrole = m.group(3)
869
      self.ldisk = m.group(4)
870
      self.rdisk = m.group(5)
871

    
872
    # end reading of data from the LINE_RE or UNCONF_RE
873

    
874
    self.is_standalone = self.cstatus == self.CS_STANDALONE
875
    self.is_wfconn = self.cstatus == self.CS_WFCONNECTION
876
    self.is_connected = self.cstatus == self.CS_CONNECTED
877
    self.is_primary = self.lrole == self.RO_PRIMARY
878
    self.is_secondary = self.lrole == self.RO_SECONDARY
879
    self.peer_primary = self.rrole == self.RO_PRIMARY
880
    self.peer_secondary = self.rrole == self.RO_SECONDARY
881
    self.both_primary = self.is_primary and self.peer_primary
882
    self.both_secondary = self.is_secondary and self.peer_secondary
883

    
884
    self.is_diskless = self.ldisk == self.DS_DISKLESS
885
    self.is_disk_uptodate = self.ldisk == self.DS_UPTODATE
886

    
887
    self.is_in_resync = self.cstatus in self.CSET_SYNC
888
    self.is_in_use = self.cstatus != self.CS_UNCONFIGURED
889

    
890
    m = self.SYNC_RE.match(procline)
891
    if m:
892
      self.sync_percent = float(m.group(1))
893
      hours = int(m.group(2))
894
      minutes = int(m.group(3))
895
      seconds = int(m.group(4))
896
      self.est_time = hours * 3600 + minutes * 60 + seconds
897
    else:
898
      # we have (in this if branch) no percent information, but if
899
      # we're resyncing we need to 'fake' a sync percent information,
900
      # as this is how cmdlib determines if it makes sense to wait for
901
      # resyncing or not
902
      if self.is_in_resync:
903
        self.sync_percent = 0
904
      else:
905
        self.sync_percent = None
906
      self.est_time = None
907

    
908

    
909
class BaseDRBD(BlockDev): # pylint: disable=W0223
910
  """Base DRBD class.
911

912
  This class contains a few bits of common functionality between the
913
  0.7 and 8.x versions of DRBD.
914

915
  """
916
  _VERSION_RE = re.compile(r"^version: (\d+)\.(\d+)\.(\d+)(?:\.\d+)?"
917
                           r" \(api:(\d+)/proto:(\d+)(?:-(\d+))?\)")
918
  _VALID_LINE_RE = re.compile("^ *([0-9]+): cs:([^ ]+).*$")
919
  _UNUSED_LINE_RE = re.compile("^ *([0-9]+): cs:Unconfigured$")
920

    
921
  _DRBD_MAJOR = 147
922
  _ST_UNCONFIGURED = "Unconfigured"
923
  _ST_WFCONNECTION = "WFConnection"
924
  _ST_CONNECTED = "Connected"
925

    
926
  _STATUS_FILE = "/proc/drbd"
927
  _USERMODE_HELPER_FILE = "/sys/module/drbd/parameters/usermode_helper"
928

    
929
  @staticmethod
930
  def _GetProcData(filename=_STATUS_FILE):
931
    """Return data from /proc/drbd.
932

933
    """
934
    try:
935
      data = utils.ReadFile(filename).splitlines()
936
    except EnvironmentError, err:
937
      if err.errno == errno.ENOENT:
938
        _ThrowError("The file %s cannot be opened, check if the module"
939
                    " is loaded (%s)", filename, str(err))
940
      else:
941
        _ThrowError("Can't read the DRBD proc file %s: %s", filename, str(err))
942
    if not data:
943
      _ThrowError("Can't read any data from %s", filename)
944
    return data
945

    
946
  @classmethod
947
  def _MassageProcData(cls, data):
948
    """Transform the output of _GetProdData into a nicer form.
949

950
    @return: a dictionary of minor: joined lines from /proc/drbd
951
        for that minor
952

953
    """
954
    results = {}
955
    old_minor = old_line = None
956
    for line in data:
957
      if not line: # completely empty lines, as can be returned by drbd8.0+
958
        continue
959
      lresult = cls._VALID_LINE_RE.match(line)
960
      if lresult is not None:
961
        if old_minor is not None:
962
          results[old_minor] = old_line
963
        old_minor = int(lresult.group(1))
964
        old_line = line
965
      else:
966
        if old_minor is not None:
967
          old_line += " " + line.strip()
968
    # add last line
969
    if old_minor is not None:
970
      results[old_minor] = old_line
971
    return results
972

    
973
  @classmethod
974
  def _GetVersion(cls, proc_data):
975
    """Return the DRBD version.
976

977
    This will return a dict with keys:
978
      - k_major
979
      - k_minor
980
      - k_point
981
      - api
982
      - proto
983
      - proto2 (only on drbd > 8.2.X)
984

985
    """
986
    first_line = proc_data[0].strip()
987
    version = cls._VERSION_RE.match(first_line)
988
    if not version:
989
      raise errors.BlockDeviceError("Can't parse DRBD version from '%s'" %
990
                                    first_line)
991

    
992
    values = version.groups()
993
    retval = {
994
      "k_major": int(values[0]),
995
      "k_minor": int(values[1]),
996
      "k_point": int(values[2]),
997
      "api": int(values[3]),
998
      "proto": int(values[4]),
999
      }
1000
    if values[5] is not None:
1001
      retval["proto2"] = values[5]
1002

    
1003
    return retval
1004

    
1005
  @staticmethod
1006
  def GetUsermodeHelper(filename=_USERMODE_HELPER_FILE):
1007
    """Returns DRBD usermode_helper currently set.
1008

1009
    """
1010
    try:
1011
      helper = utils.ReadFile(filename).splitlines()[0]
1012
    except EnvironmentError, err:
1013
      if err.errno == errno.ENOENT:
1014
        _ThrowError("The file %s cannot be opened, check if the module"
1015
                    " is loaded (%s)", filename, str(err))
1016
      else:
1017
        _ThrowError("Can't read DRBD helper file %s: %s", filename, str(err))
1018
    if not helper:
1019
      _ThrowError("Can't read any data from %s", filename)
1020
    return helper
1021

    
1022
  @staticmethod
1023
  def _DevPath(minor):
1024
    """Return the path to a drbd device for a given minor.
1025

1026
    """
1027
    return "/dev/drbd%d" % minor
1028

    
1029
  @classmethod
1030
  def GetUsedDevs(cls):
1031
    """Compute the list of used DRBD devices.
1032

1033
    """
1034
    data = cls._GetProcData()
1035

    
1036
    used_devs = {}
1037
    for line in data:
1038
      match = cls._VALID_LINE_RE.match(line)
1039
      if not match:
1040
        continue
1041
      minor = int(match.group(1))
1042
      state = match.group(2)
1043
      if state == cls._ST_UNCONFIGURED:
1044
        continue
1045
      used_devs[minor] = state, line
1046

    
1047
    return used_devs
1048

    
1049
  def _SetFromMinor(self, minor):
1050
    """Set our parameters based on the given minor.
1051

1052
    This sets our minor variable and our dev_path.
1053

1054
    """
1055
    if minor is None:
1056
      self.minor = self.dev_path = None
1057
      self.attached = False
1058
    else:
1059
      self.minor = minor
1060
      self.dev_path = self._DevPath(minor)
1061
      self.attached = True
1062

    
1063
  @staticmethod
1064
  def _CheckMetaSize(meta_device):
1065
    """Check if the given meta device looks like a valid one.
1066

1067
    This currently only checks the size, which must be around
1068
    128MiB.
1069

1070
    """
1071
    result = utils.RunCmd(["blockdev", "--getsize", meta_device])
1072
    if result.failed:
1073
      _ThrowError("Failed to get device size: %s - %s",
1074
                  result.fail_reason, result.output)
1075
    try:
1076
      sectors = int(result.stdout)
1077
    except (TypeError, ValueError):
1078
      _ThrowError("Invalid output from blockdev: '%s'", result.stdout)
1079
    num_bytes = sectors * 512
1080
    if num_bytes < 128 * 1024 * 1024: # less than 128MiB
1081
      _ThrowError("Meta device too small (%.2fMib)", (num_bytes / 1024 / 1024))
1082
    # the maximum *valid* size of the meta device when living on top
1083
    # of LVM is hard to compute: it depends on the number of stripes
1084
    # and the PE size; e.g. a 2-stripe, 64MB PE will result in a 128MB
1085
    # (normal size), but an eight-stripe 128MB PE will result in a 1GB
1086
    # size meta device; as such, we restrict it to 1GB (a little bit
1087
    # too generous, but making assumptions about PE size is hard)
1088
    if num_bytes > 1024 * 1024 * 1024:
1089
      _ThrowError("Meta device too big (%.2fMiB)", (num_bytes / 1024 / 1024))
1090

    
1091
  def Rename(self, new_id):
1092
    """Rename a device.
1093

1094
    This is not supported for drbd devices.
1095

1096
    """
1097
    raise errors.ProgrammerError("Can't rename a drbd device")
1098

    
1099

    
1100
class DRBD8(BaseDRBD):
1101
  """DRBD v8.x block device.
1102

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

1107
  The unique_id for the drbd device is a (local_ip, local_port,
1108
  remote_ip, remote_port, local_minor, secret) tuple, and it must have
1109
  two children: the data device and the meta_device. The meta device
1110
  is checked for valid size and is zeroed on create.
1111

1112
  """
1113
  _MAX_MINORS = 255
1114
  _PARSE_SHOW = None
1115

    
1116
  # timeout constants
1117
  _NET_RECONFIG_TIMEOUT = 60
1118

    
1119
  # command line options for barriers
1120
  _DISABLE_DISK_OPTION = "--no-disk-barrier"  # -a
1121
  _DISABLE_DRAIN_OPTION = "--no-disk-drain"   # -D
1122
  _DISABLE_FLUSH_OPTION = "--no-disk-flushes" # -i
1123
  _DISABLE_META_FLUSH_OPTION = "--no-md-flushes"  # -m
1124

    
1125
  def __init__(self, unique_id, children, size, params):
1126
    if children and children.count(None) > 0:
1127
      children = []
1128
    if len(children) not in (0, 2):
1129
      raise ValueError("Invalid configuration data %s" % str(children))
1130
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 6:
1131
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1132
    (self._lhost, self._lport,
1133
     self._rhost, self._rport,
1134
     self._aminor, self._secret) = unique_id
1135
    if children:
1136
      if not _CanReadDevice(children[1].dev_path):
1137
        logging.info("drbd%s: Ignoring unreadable meta device", self._aminor)
1138
        children = []
1139
    super(DRBD8, self).__init__(unique_id, children, size, params)
1140
    self.major = self._DRBD_MAJOR
1141
    version = self._GetVersion(self._GetProcData())
1142
    if version["k_major"] != 8:
1143
      _ThrowError("Mismatch in DRBD kernel version and requested ganeti"
1144
                  " usage: kernel is %s.%s, ganeti wants 8.x",
1145
                  version["k_major"], version["k_minor"])
1146

    
1147
    if (self._lhost is not None and self._lhost == self._rhost and
1148
        self._lport == self._rport):
1149
      raise ValueError("Invalid configuration data, same local/remote %s" %
1150
                       (unique_id,))
1151
    self.Attach()
1152

    
1153
  @classmethod
1154
  def _InitMeta(cls, minor, dev_path):
1155
    """Initialize a meta device.
1156

1157
    This will not work if the given minor is in use.
1158

1159
    """
1160
    # Zero the metadata first, in order to make sure drbdmeta doesn't
1161
    # try to auto-detect existing filesystems or similar (see
1162
    # http://code.google.com/p/ganeti/issues/detail?id=182); we only
1163
    # care about the first 128MB of data in the device, even though it
1164
    # can be bigger
1165
    result = utils.RunCmd([constants.DD_CMD,
1166
                           "if=/dev/zero", "of=%s" % dev_path,
1167
                           "bs=1048576", "count=128", "oflag=direct"])
1168
    if result.failed:
1169
      _ThrowError("Can't wipe the meta device: %s", result.output)
1170

    
1171
    result = utils.RunCmd(["drbdmeta", "--force", cls._DevPath(minor),
1172
                           "v08", dev_path, "0", "create-md"])
1173
    if result.failed:
1174
      _ThrowError("Can't initialize meta device: %s", result.output)
1175

    
1176
  @classmethod
1177
  def _FindUnusedMinor(cls):
1178
    """Find an unused DRBD device.
1179

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

1183
    """
1184
    data = cls._GetProcData()
1185

    
1186
    highest = None
1187
    for line in data:
1188
      match = cls._UNUSED_LINE_RE.match(line)
1189
      if match:
1190
        return int(match.group(1))
1191
      match = cls._VALID_LINE_RE.match(line)
1192
      if match:
1193
        minor = int(match.group(1))
1194
        highest = max(highest, minor)
1195
    if highest is None: # there are no minors in use at all
1196
      return 0
1197
    if highest >= cls._MAX_MINORS:
1198
      logging.error("Error: no free drbd minors!")
1199
      raise errors.BlockDeviceError("Can't find a free DRBD minor")
1200
    return highest + 1
1201

    
1202
  @classmethod
1203
  def _GetShowParser(cls):
1204
    """Return a parser for `drbd show` output.
1205

1206
    This will either create or return an already-created parser for the
1207
    output of the command `drbd show`.
1208

1209
    """
1210
    if cls._PARSE_SHOW is not None:
1211
      return cls._PARSE_SHOW
1212

    
1213
    # pyparsing setup
1214
    lbrace = pyp.Literal("{").suppress()
1215
    rbrace = pyp.Literal("}").suppress()
1216
    lbracket = pyp.Literal("[").suppress()
1217
    rbracket = pyp.Literal("]").suppress()
1218
    semi = pyp.Literal(";").suppress()
1219
    colon = pyp.Literal(":").suppress()
1220
    # this also converts the value to an int
1221
    number = pyp.Word(pyp.nums).setParseAction(lambda s, l, t: int(t[0]))
1222

    
1223
    comment = pyp.Literal("#") + pyp.Optional(pyp.restOfLine)
1224
    defa = pyp.Literal("_is_default").suppress()
1225
    dbl_quote = pyp.Literal('"').suppress()
1226

    
1227
    keyword = pyp.Word(pyp.alphanums + "-")
1228

    
1229
    # value types
1230
    value = pyp.Word(pyp.alphanums + "_-/.:")
1231
    quoted = dbl_quote + pyp.CharsNotIn('"') + dbl_quote
1232
    ipv4_addr = (pyp.Optional(pyp.Literal("ipv4")).suppress() +
1233
                 pyp.Word(pyp.nums + ".") + colon + number)
1234
    ipv6_addr = (pyp.Optional(pyp.Literal("ipv6")).suppress() +
1235
                 pyp.Optional(lbracket) + pyp.Word(pyp.hexnums + ":") +
1236
                 pyp.Optional(rbracket) + colon + number)
1237
    # meta device, extended syntax
1238
    meta_value = ((value ^ quoted) + lbracket + number + rbracket)
1239
    # device name, extended syntax
1240
    device_value = pyp.Literal("minor").suppress() + number
1241

    
1242
    # a statement
1243
    stmt = (~rbrace + keyword + ~lbrace +
1244
            pyp.Optional(ipv4_addr ^ ipv6_addr ^ value ^ quoted ^ meta_value ^
1245
                         device_value) +
1246
            pyp.Optional(defa) + semi +
1247
            pyp.Optional(pyp.restOfLine).suppress())
1248

    
1249
    # an entire section
1250
    section_name = pyp.Word(pyp.alphas + "_")
1251
    section = section_name + lbrace + pyp.ZeroOrMore(pyp.Group(stmt)) + rbrace
1252

    
1253
    bnf = pyp.ZeroOrMore(pyp.Group(section ^ stmt))
1254
    bnf.ignore(comment)
1255

    
1256
    cls._PARSE_SHOW = bnf
1257

    
1258
    return bnf
1259

    
1260
  @classmethod
1261
  def _GetShowData(cls, minor):
1262
    """Return the `drbdsetup show` data for a minor.
1263

1264
    """
1265
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "show"])
1266
    if result.failed:
1267
      logging.error("Can't display the drbd config: %s - %s",
1268
                    result.fail_reason, result.output)
1269
      return None
1270
    return result.stdout
1271

    
1272
  @classmethod
1273
  def _GetDevInfo(cls, out):
1274
    """Parse details about a given DRBD minor.
1275

1276
    This return, if available, the local backing device (as a path)
1277
    and the local and remote (ip, port) information from a string
1278
    containing the output of the `drbdsetup show` command as returned
1279
    by _GetShowData.
1280

1281
    """
1282
    data = {}
1283
    if not out:
1284
      return data
1285

    
1286
    bnf = cls._GetShowParser()
1287
    # run pyparse
1288

    
1289
    try:
1290
      results = bnf.parseString(out)
1291
    except pyp.ParseException, err:
1292
      _ThrowError("Can't parse drbdsetup show output: %s", str(err))
1293

    
1294
    # and massage the results into our desired format
1295
    for section in results:
1296
      sname = section[0]
1297
      if sname == "_this_host":
1298
        for lst in section[1:]:
1299
          if lst[0] == "disk":
1300
            data["local_dev"] = lst[1]
1301
          elif lst[0] == "meta-disk":
1302
            data["meta_dev"] = lst[1]
1303
            data["meta_index"] = lst[2]
1304
          elif lst[0] == "address":
1305
            data["local_addr"] = tuple(lst[1:])
1306
      elif sname == "_remote_host":
1307
        for lst in section[1:]:
1308
          if lst[0] == "address":
1309
            data["remote_addr"] = tuple(lst[1:])
1310
    return data
1311

    
1312
  def _MatchesLocal(self, info):
1313
    """Test if our local config matches with an existing device.
1314

1315
    The parameter should be as returned from `_GetDevInfo()`. This
1316
    method tests if our local backing device is the same as the one in
1317
    the info parameter, in effect testing if we look like the given
1318
    device.
1319

1320
    """
1321
    if self._children:
1322
      backend, meta = self._children
1323
    else:
1324
      backend = meta = None
1325

    
1326
    if backend is not None:
1327
      retval = ("local_dev" in info and info["local_dev"] == backend.dev_path)
1328
    else:
1329
      retval = ("local_dev" not in info)
1330

    
1331
    if meta is not None:
1332
      retval = retval and ("meta_dev" in info and
1333
                           info["meta_dev"] == meta.dev_path)
1334
      retval = retval and ("meta_index" in info and
1335
                           info["meta_index"] == 0)
1336
    else:
1337
      retval = retval and ("meta_dev" not in info and
1338
                           "meta_index" not in info)
1339
    return retval
1340

    
1341
  def _MatchesNet(self, info):
1342
    """Test if our network config matches with an existing device.
1343

1344
    The parameter should be as returned from `_GetDevInfo()`. This
1345
    method tests if our network configuration is the same as the one
1346
    in the info parameter, in effect testing if we look like the given
1347
    device.
1348

1349
    """
1350
    if (((self._lhost is None and not ("local_addr" in info)) and
1351
         (self._rhost is None and not ("remote_addr" in info)))):
1352
      return True
1353

    
1354
    if self._lhost is None:
1355
      return False
1356

    
1357
    if not ("local_addr" in info and
1358
            "remote_addr" in info):
1359
      return False
1360

    
1361
    retval = (info["local_addr"] == (self._lhost, self._lport))
1362
    retval = (retval and
1363
              info["remote_addr"] == (self._rhost, self._rport))
1364
    return retval
1365

    
1366
  def _AssembleLocal(self, minor, backend, meta, size):
1367
    """Configure the local part of a DRBD device.
1368

1369
    """
1370
    args = ["drbdsetup", self._DevPath(minor), "disk",
1371
            backend, meta, "0",
1372
            "-e", "detach",
1373
            "--create-device"]
1374
    if size:
1375
      args.extend(["-d", "%sm" % size])
1376

    
1377
    version = self._GetVersion(self._GetProcData())
1378
    vmaj = version["k_major"]
1379
    vmin = version["k_minor"]
1380
    vrel = version["k_point"]
1381

    
1382
    barrier_args = \
1383
      self._ComputeDiskBarrierArgs(vmaj, vmin, vrel,
1384
                                   self.params[constants.LDP_BARRIERS],
1385
                                   self.params[constants.LDP_NO_META_FLUSH])
1386
    args.extend(barrier_args)
1387

    
1388
    if self.params[constants.LDP_DISK_CUSTOM]:
1389
      args.extend(shlex.split(self.params[constants.LDP_DISK_CUSTOM]))
1390

    
1391
    result = utils.RunCmd(args)
1392
    if result.failed:
1393
      _ThrowError("drbd%d: can't attach local disk: %s", minor, result.output)
1394

    
1395
  @classmethod
1396
  def _ComputeDiskBarrierArgs(cls, vmaj, vmin, vrel, disabled_barriers,
1397
                              disable_meta_flush):
1398
    """Compute the DRBD command line parameters for disk barriers
1399

1400
    Returns a list of the disk barrier parameters as requested via the
1401
    disabled_barriers and disable_meta_flush arguments, and according to the
1402
    supported ones in the DRBD version vmaj.vmin.vrel
1403

1404
    If the desired option is unsupported, raises errors.BlockDeviceError.
1405

1406
    """
1407
    disabled_barriers_set = frozenset(disabled_barriers)
1408
    if not disabled_barriers_set in constants.DRBD_VALID_BARRIER_OPT:
1409
      raise errors.BlockDeviceError("%s is not a valid option set for DRBD"
1410
                                    " barriers" % disabled_barriers)
1411

    
1412
    args = []
1413

    
1414
    # The following code assumes DRBD 8.x, with x < 4 and x != 1 (DRBD 8.1.x
1415
    # does not exist)
1416
    if not vmaj == 8 and vmin in (0, 2, 3):
1417
      raise errors.BlockDeviceError("Unsupported DRBD version: %d.%d.%d" %
1418
                                    (vmaj, vmin, vrel))
1419

    
1420
    def _AppendOrRaise(option, min_version):
1421
      """Helper for DRBD options"""
1422
      if min_version is not None and vrel >= min_version:
1423
        args.append(option)
1424
      else:
1425
        raise errors.BlockDeviceError("Could not use the option %s as the"
1426
                                      " DRBD version %d.%d.%d does not support"
1427
                                      " it." % (option, vmaj, vmin, vrel))
1428

    
1429
    # the minimum version for each feature is encoded via pairs of (minor
1430
    # version -> x) where x is version in which support for the option was
1431
    # introduced.
1432
    meta_flush_supported = disk_flush_supported = {
1433
      0: 12,
1434
      2: 7,
1435
      3: 0,
1436
      }
1437

    
1438
    disk_drain_supported = {
1439
      2: 7,
1440
      3: 0,
1441
      }
1442

    
1443
    disk_barriers_supported = {
1444
      3: 0,
1445
      }
1446

    
1447
    # meta flushes
1448
    if disable_meta_flush:
1449
      _AppendOrRaise(cls._DISABLE_META_FLUSH_OPTION,
1450
                     meta_flush_supported.get(vmin, None))
1451

    
1452
    # disk flushes
1453
    if constants.DRBD_B_DISK_FLUSH in disabled_barriers_set:
1454
      _AppendOrRaise(cls._DISABLE_FLUSH_OPTION,
1455
                     disk_flush_supported.get(vmin, None))
1456

    
1457
    # disk drain
1458
    if constants.DRBD_B_DISK_DRAIN in disabled_barriers_set:
1459
      _AppendOrRaise(cls._DISABLE_DRAIN_OPTION,
1460
                     disk_drain_supported.get(vmin, None))
1461

    
1462
    # disk barriers
1463
    if constants.DRBD_B_DISK_BARRIERS in disabled_barriers_set:
1464
      _AppendOrRaise(cls._DISABLE_DISK_OPTION,
1465
                     disk_barriers_supported.get(vmin, None))
1466

    
1467
    return args
1468

    
1469
  def _AssembleNet(self, minor, net_info, protocol,
1470
                   dual_pri=False, hmac=None, secret=None):
1471
    """Configure the network part of the device.
1472

1473
    """
1474
    lhost, lport, rhost, rport = net_info
1475
    if None in net_info:
1476
      # we don't want network connection and actually want to make
1477
      # sure its shutdown
1478
      self._ShutdownNet(minor)
1479
      return
1480

    
1481
    # Workaround for a race condition. When DRBD is doing its dance to
1482
    # establish a connection with its peer, it also sends the
1483
    # synchronization speed over the wire. In some cases setting the
1484
    # sync speed only after setting up both sides can race with DRBD
1485
    # connecting, hence we set it here before telling DRBD anything
1486
    # about its peer.
1487
    sync_errors = self._SetMinorSyncParams(minor, self.params)
1488
    if sync_errors:
1489
      _ThrowError("drbd%d: can't set the synchronization parameters: %s" %
1490
                  (minor, utils.CommaJoin(sync_errors)))
1491

    
1492
    if netutils.IP6Address.IsValid(lhost):
1493
      if not netutils.IP6Address.IsValid(rhost):
1494
        _ThrowError("drbd%d: can't connect ip %s to ip %s" %
1495
                    (minor, lhost, rhost))
1496
      family = "ipv6"
1497
    elif netutils.IP4Address.IsValid(lhost):
1498
      if not netutils.IP4Address.IsValid(rhost):
1499
        _ThrowError("drbd%d: can't connect ip %s to ip %s" %
1500
                    (minor, lhost, rhost))
1501
      family = "ipv4"
1502
    else:
1503
      _ThrowError("drbd%d: Invalid ip %s" % (minor, lhost))
1504

    
1505
    args = ["drbdsetup", self._DevPath(minor), "net",
1506
            "%s:%s:%s" % (family, lhost, lport),
1507
            "%s:%s:%s" % (family, rhost, rport), protocol,
1508
            "-A", "discard-zero-changes",
1509
            "-B", "consensus",
1510
            "--create-device",
1511
            ]
1512
    if dual_pri:
1513
      args.append("-m")
1514
    if hmac and secret:
1515
      args.extend(["-a", hmac, "-x", secret])
1516

    
1517
    if self.params[constants.LDP_NET_CUSTOM]:
1518
      args.extend(shlex.split(self.params[constants.LDP_NET_CUSTOM]))
1519

    
1520
    result = utils.RunCmd(args)
1521
    if result.failed:
1522
      _ThrowError("drbd%d: can't setup network: %s - %s",
1523
                  minor, result.fail_reason, result.output)
1524

    
1525
    def _CheckNetworkConfig():
1526
      info = self._GetDevInfo(self._GetShowData(minor))
1527
      if not "local_addr" in info or not "remote_addr" in info:
1528
        raise utils.RetryAgain()
1529

    
1530
      if (info["local_addr"] != (lhost, lport) or
1531
          info["remote_addr"] != (rhost, rport)):
1532
        raise utils.RetryAgain()
1533

    
1534
    try:
1535
      utils.Retry(_CheckNetworkConfig, 1.0, 10.0)
1536
    except utils.RetryTimeout:
1537
      _ThrowError("drbd%d: timeout while configuring network", minor)
1538

    
1539
  def AddChildren(self, devices):
1540
    """Add a disk to the DRBD device.
1541

1542
    """
1543
    if self.minor is None:
1544
      _ThrowError("drbd%d: can't attach to dbrd8 during AddChildren",
1545
                  self._aminor)
1546
    if len(devices) != 2:
1547
      _ThrowError("drbd%d: need two devices for AddChildren", self.minor)
1548
    info = self._GetDevInfo(self._GetShowData(self.minor))
1549
    if "local_dev" in info:
1550
      _ThrowError("drbd%d: already attached to a local disk", self.minor)
1551
    backend, meta = devices
1552
    if backend.dev_path is None or meta.dev_path is None:
1553
      _ThrowError("drbd%d: children not ready during AddChildren", self.minor)
1554
    backend.Open()
1555
    meta.Open()
1556
    self._CheckMetaSize(meta.dev_path)
1557
    self._InitMeta(self._FindUnusedMinor(), meta.dev_path)
1558

    
1559
    self._AssembleLocal(self.minor, backend.dev_path, meta.dev_path, self.size)
1560
    self._children = devices
1561

    
1562
  def RemoveChildren(self, devices):
1563
    """Detach the drbd device from local storage.
1564

1565
    """
1566
    if self.minor is None:
1567
      _ThrowError("drbd%d: can't attach to drbd8 during RemoveChildren",
1568
                  self._aminor)
1569
    # early return if we don't actually have backing storage
1570
    info = self._GetDevInfo(self._GetShowData(self.minor))
1571
    if "local_dev" not in info:
1572
      return
1573
    if len(self._children) != 2:
1574
      _ThrowError("drbd%d: we don't have two children: %s", self.minor,
1575
                  self._children)
1576
    if self._children.count(None) == 2: # we don't actually have children :)
1577
      logging.warning("drbd%d: requested detach while detached", self.minor)
1578
      return
1579
    if len(devices) != 2:
1580
      _ThrowError("drbd%d: we need two children in RemoveChildren", self.minor)
1581
    for child, dev in zip(self._children, devices):
1582
      if dev != child.dev_path:
1583
        _ThrowError("drbd%d: mismatch in local storage (%s != %s) in"
1584
                    " RemoveChildren", self.minor, dev, child.dev_path)
1585

    
1586
    self._ShutdownLocal(self.minor)
1587
    self._children = []
1588

    
1589
  @classmethod
1590
  def _SetMinorSyncParams(cls, minor, params):
1591
    """Set the parameters of the DRBD syncer.
1592

1593
    This is the low-level implementation.
1594

1595
    @type minor: int
1596
    @param minor: the drbd minor whose settings we change
1597
    @type params: dict
1598
    @param params: LD level disk parameters related to the synchronization
1599
    @rtype: list
1600
    @return: a list of error messages
1601

1602
    """
1603

    
1604
    args = ["drbdsetup", cls._DevPath(minor), "syncer"]
1605
    if params[constants.LDP_DYNAMIC_RESYNC]:
1606
      version = cls._GetVersion(cls._GetProcData())
1607
      vmin = version["k_minor"]
1608
      vrel = version["k_point"]
1609

    
1610
      # By definition we are using 8.x, so just check the rest of the version
1611
      # number
1612
      if vmin != 3 or vrel < 9:
1613
        msg = ("The current DRBD version (8.%d.%d) does not support the "
1614
               "dynamic resync speed controller" % (vmin, vrel))
1615
        logging.error(msg)
1616
        return [msg]
1617

    
1618
      if params[constants.LDP_PLAN_AHEAD] == 0:
1619
        msg = ("A value of 0 for c-plan-ahead disables the dynamic sync speed"
1620
               " controller at DRBD level. If you want to disable it, please"
1621
               " set the dynamic-resync disk parameter to False.")
1622
        logging.error(msg)
1623
        return [msg]
1624

    
1625
      # add the c-* parameters to args
1626
      args.extend(["--c-plan-ahead", params[constants.LDP_PLAN_AHEAD],
1627
                   "--c-fill-target", params[constants.LDP_FILL_TARGET],
1628
                   "--c-delay-target", params[constants.LDP_DELAY_TARGET],
1629
                   "--c-max-rate", params[constants.LDP_MAX_RATE],
1630
                   "--c-min-rate", params[constants.LDP_MIN_RATE],
1631
                   ])
1632

    
1633
    else:
1634
      args.extend(["-r", "%d" % params[constants.LDP_RESYNC_RATE]])
1635

    
1636
    args.append("--create-device")
1637
    result = utils.RunCmd(args)
1638
    if result.failed:
1639
      msg = ("Can't change syncer rate: %s - %s" %
1640
             (result.fail_reason, result.output))
1641
      logging.error(msg)
1642
      return [msg]
1643

    
1644
    return []
1645

    
1646
  def SetSyncParams(self, params):
1647
    """Set the synchronization parameters of the DRBD syncer.
1648

1649
    @type params: dict
1650
    @param params: LD level disk parameters related to the synchronization
1651
    @rtype: list
1652
    @return: a list of error messages, emitted both by the current node and by
1653
    children. An empty list means no errors
1654

1655
    """
1656
    if self.minor is None:
1657
      err = "Not attached during SetSyncParams"
1658
      logging.info(err)
1659
      return [err]
1660

    
1661
    children_result = super(DRBD8, self).SetSyncParams(params)
1662
    children_result.extend(self._SetMinorSyncParams(self.minor, params))
1663
    return children_result
1664

    
1665
  def PauseResumeSync(self, pause):
1666
    """Pauses or resumes the sync of a DRBD device.
1667

1668
    @param pause: Wether to pause or resume
1669
    @return: the success of the operation
1670

1671
    """
1672
    if self.minor is None:
1673
      logging.info("Not attached during PauseSync")
1674
      return False
1675

    
1676
    children_result = super(DRBD8, self).PauseResumeSync(pause)
1677

    
1678
    if pause:
1679
      cmd = "pause-sync"
1680
    else:
1681
      cmd = "resume-sync"
1682

    
1683
    result = utils.RunCmd(["drbdsetup", self.dev_path, cmd])
1684
    if result.failed:
1685
      logging.error("Can't %s: %s - %s", cmd,
1686
                    result.fail_reason, result.output)
1687
    return not result.failed and children_result
1688

    
1689
  def GetProcStatus(self):
1690
    """Return device data from /proc.
1691

1692
    """
1693
    if self.minor is None:
1694
      _ThrowError("drbd%d: GetStats() called while not attached", self._aminor)
1695
    proc_info = self._MassageProcData(self._GetProcData())
1696
    if self.minor not in proc_info:
1697
      _ThrowError("drbd%d: can't find myself in /proc", self.minor)
1698
    return DRBD8Status(proc_info[self.minor])
1699

    
1700
  def GetSyncStatus(self):
1701
    """Returns the sync status of the device.
1702

1703

1704
    If sync_percent is None, it means all is ok
1705
    If estimated_time is None, it means we can't estimate
1706
    the time needed, otherwise it's the time left in seconds.
1707

1708

1709
    We set the is_degraded parameter to True on two conditions:
1710
    network not connected or local disk missing.
1711

1712
    We compute the ldisk parameter based on whether we have a local
1713
    disk or not.
1714

1715
    @rtype: objects.BlockDevStatus
1716

1717
    """
1718
    if self.minor is None and not self.Attach():
1719
      _ThrowError("drbd%d: can't Attach() in GetSyncStatus", self._aminor)
1720

    
1721
    stats = self.GetProcStatus()
1722
    is_degraded = not stats.is_connected or not stats.is_disk_uptodate
1723

    
1724
    if stats.is_disk_uptodate:
1725
      ldisk_status = constants.LDS_OKAY
1726
    elif stats.is_diskless:
1727
      ldisk_status = constants.LDS_FAULTY
1728
    else:
1729
      ldisk_status = constants.LDS_UNKNOWN
1730

    
1731
    return objects.BlockDevStatus(dev_path=self.dev_path,
1732
                                  major=self.major,
1733
                                  minor=self.minor,
1734
                                  sync_percent=stats.sync_percent,
1735
                                  estimated_time=stats.est_time,
1736
                                  is_degraded=is_degraded,
1737
                                  ldisk_status=ldisk_status)
1738

    
1739
  def Open(self, force=False):
1740
    """Make the local state primary.
1741

1742
    If the 'force' parameter is given, the '-o' option is passed to
1743
    drbdsetup. Since this is a potentially dangerous operation, the
1744
    force flag should be only given after creation, when it actually
1745
    is mandatory.
1746

1747
    """
1748
    if self.minor is None and not self.Attach():
1749
      logging.error("DRBD cannot attach to a device during open")
1750
      return False
1751
    cmd = ["drbdsetup", self.dev_path, "primary"]
1752
    if force:
1753
      cmd.append("-o")
1754
    result = utils.RunCmd(cmd)
1755
    if result.failed:
1756
      _ThrowError("drbd%d: can't make drbd device primary: %s", self.minor,
1757
                  result.output)
1758

    
1759
  def Close(self):
1760
    """Make the local state secondary.
1761

1762
    This will, of course, fail if the device is in use.
1763

1764
    """
1765
    if self.minor is None and not self.Attach():
1766
      _ThrowError("drbd%d: can't Attach() in Close()", self._aminor)
1767
    result = utils.RunCmd(["drbdsetup", self.dev_path, "secondary"])
1768
    if result.failed:
1769
      _ThrowError("drbd%d: can't switch drbd device to secondary: %s",
1770
                  self.minor, result.output)
1771

    
1772
  def DisconnectNet(self):
1773
    """Removes network configuration.
1774

1775
    This method shutdowns the network side of the device.
1776

1777
    The method will wait up to a hardcoded timeout for the device to
1778
    go into standalone after the 'disconnect' command before
1779
    re-configuring it, as sometimes it takes a while for the
1780
    disconnect to actually propagate and thus we might issue a 'net'
1781
    command while the device is still connected. If the device will
1782
    still be attached to the network and we time out, we raise an
1783
    exception.
1784

1785
    """
1786
    if self.minor is None:
1787
      _ThrowError("drbd%d: disk not attached in re-attach net", self._aminor)
1788

    
1789
    if None in (self._lhost, self._lport, self._rhost, self._rport):
1790
      _ThrowError("drbd%d: DRBD disk missing network info in"
1791
                  " DisconnectNet()", self.minor)
1792

    
1793
    class _DisconnectStatus:
1794
      def __init__(self, ever_disconnected):
1795
        self.ever_disconnected = ever_disconnected
1796

    
1797
    dstatus = _DisconnectStatus(_IgnoreError(self._ShutdownNet, self.minor))
1798

    
1799
    def _WaitForDisconnect():
1800
      if self.GetProcStatus().is_standalone:
1801
        return
1802

    
1803
      # retry the disconnect, it seems possible that due to a well-time
1804
      # disconnect on the peer, my disconnect command might be ignored and
1805
      # forgotten
1806
      dstatus.ever_disconnected = \
1807
        _IgnoreError(self._ShutdownNet, self.minor) or dstatus.ever_disconnected
1808

    
1809
      raise utils.RetryAgain()
1810

    
1811
    # Keep start time
1812
    start_time = time.time()
1813

    
1814
    try:
1815
      # Start delay at 100 milliseconds and grow up to 2 seconds
1816
      utils.Retry(_WaitForDisconnect, (0.1, 1.5, 2.0),
1817
                  self._NET_RECONFIG_TIMEOUT)
1818
    except utils.RetryTimeout:
1819
      if dstatus.ever_disconnected:
1820
        msg = ("drbd%d: device did not react to the"
1821
               " 'disconnect' command in a timely manner")
1822
      else:
1823
        msg = "drbd%d: can't shutdown network, even after multiple retries"
1824

    
1825
      _ThrowError(msg, self.minor)
1826

    
1827
    reconfig_time = time.time() - start_time
1828
    if reconfig_time > (self._NET_RECONFIG_TIMEOUT * 0.25):
1829
      logging.info("drbd%d: DisconnectNet: detach took %.3f seconds",
1830
                   self.minor, reconfig_time)
1831

    
1832
  def AttachNet(self, multimaster):
1833
    """Reconnects the network.
1834

1835
    This method connects the network side of the device with a
1836
    specified multi-master flag. The device needs to be 'Standalone'
1837
    but have valid network configuration data.
1838

1839
    Args:
1840
      - multimaster: init the network in dual-primary mode
1841

1842
    """
1843
    if self.minor is None:
1844
      _ThrowError("drbd%d: device not attached in AttachNet", self._aminor)
1845

    
1846
    if None in (self._lhost, self._lport, self._rhost, self._rport):
1847
      _ThrowError("drbd%d: missing network info in AttachNet()", self.minor)
1848

    
1849
    status = self.GetProcStatus()
1850

    
1851
    if not status.is_standalone:
1852
      _ThrowError("drbd%d: device is not standalone in AttachNet", self.minor)
1853

    
1854
    self._AssembleNet(self.minor,
1855
                      (self._lhost, self._lport, self._rhost, self._rport),
1856
                      constants.DRBD_NET_PROTOCOL, dual_pri=multimaster,
1857
                      hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1858

    
1859
  def Attach(self):
1860
    """Check if our minor is configured.
1861

1862
    This doesn't do any device configurations - it only checks if the
1863
    minor is in a state different from Unconfigured.
1864

1865
    Note that this function will not change the state of the system in
1866
    any way (except in case of side-effects caused by reading from
1867
    /proc).
1868

1869
    """
1870
    used_devs = self.GetUsedDevs()
1871
    if self._aminor in used_devs:
1872
      minor = self._aminor
1873
    else:
1874
      minor = None
1875

    
1876
    self._SetFromMinor(minor)
1877
    return minor is not None
1878

    
1879
  def Assemble(self):
1880
    """Assemble the drbd.
1881

1882
    Method:
1883
      - if we have a configured device, we try to ensure that it matches
1884
        our config
1885
      - if not, we create it from zero
1886
      - anyway, set the device parameters
1887

1888
    """
1889
    super(DRBD8, self).Assemble()
1890

    
1891
    self.Attach()
1892
    if self.minor is None:
1893
      # local device completely unconfigured
1894
      self._FastAssemble()
1895
    else:
1896
      # we have to recheck the local and network status and try to fix
1897
      # the device
1898
      self._SlowAssemble()
1899

    
1900
    sync_errors = self.SetSyncParams(self.params)
1901
    if sync_errors:
1902
      _ThrowError("drbd%d: can't set the synchronization parameters: %s" %
1903
                  (self.minor, utils.CommaJoin(sync_errors)))
1904

    
1905
  def _SlowAssemble(self):
1906
    """Assembles the DRBD device from a (partially) configured device.
1907

1908
    In case of partially attached (local device matches but no network
1909
    setup), we perform the network attach. If successful, we re-test
1910
    the attach if can return success.
1911

1912
    """
1913
    # TODO: Rewrite to not use a for loop just because there is 'break'
1914
    # pylint: disable=W0631
1915
    net_data = (self._lhost, self._lport, self._rhost, self._rport)
1916
    for minor in (self._aminor,):
1917
      info = self._GetDevInfo(self._GetShowData(minor))
1918
      match_l = self._MatchesLocal(info)
1919
      match_r = self._MatchesNet(info)
1920

    
1921
      if match_l and match_r:
1922
        # everything matches
1923
        break
1924

    
1925
      if match_l and not match_r and "local_addr" not in info:
1926
        # disk matches, but not attached to network, attach and recheck
1927
        self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
1928
                          hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1929
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1930
          break
1931
        else:
1932
          _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
1933
                      " show' disagrees", minor)
1934

    
1935
      if match_r and "local_dev" not in info:
1936
        # no local disk, but network attached and it matches
1937
        self._AssembleLocal(minor, self._children[0].dev_path,
1938
                            self._children[1].dev_path, self.size)
1939
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1940
          break
1941
        else:
1942
          _ThrowError("drbd%d: disk attach successful, but 'drbdsetup"
1943
                      " show' disagrees", minor)
1944

    
1945
      # this case must be considered only if we actually have local
1946
      # storage, i.e. not in diskless mode, because all diskless
1947
      # devices are equal from the point of view of local
1948
      # configuration
1949
      if (match_l and "local_dev" in info and
1950
          not match_r and "local_addr" in info):
1951
        # strange case - the device network part points to somewhere
1952
        # else, even though its local storage is ours; as we own the
1953
        # drbd space, we try to disconnect from the remote peer and
1954
        # reconnect to our correct one
1955
        try:
1956
          self._ShutdownNet(minor)
1957
        except errors.BlockDeviceError, err:
1958
          _ThrowError("drbd%d: device has correct local storage, wrong"
1959
                      " remote peer and is unable to disconnect in order"
1960
                      " to attach to the correct peer: %s", minor, str(err))
1961
        # note: _AssembleNet also handles the case when we don't want
1962
        # local storage (i.e. one or more of the _[lr](host|port) is
1963
        # None)
1964
        self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
1965
                          hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1966
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1967
          break
1968
        else:
1969
          _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
1970
                      " show' disagrees", minor)
1971

    
1972
    else:
1973
      minor = None
1974

    
1975
    self._SetFromMinor(minor)
1976
    if minor is None:
1977
      _ThrowError("drbd%d: cannot activate, unknown or unhandled reason",
1978
                  self._aminor)
1979

    
1980
  def _FastAssemble(self):
1981
    """Assemble the drbd device from zero.
1982

1983
    This is run when in Assemble we detect our minor is unused.
1984

1985
    """
1986
    minor = self._aminor
1987
    if self._children and self._children[0] and self._children[1]:
1988
      self._AssembleLocal(minor, self._children[0].dev_path,
1989
                          self._children[1].dev_path, self.size)
1990
    if self._lhost and self._lport and self._rhost and self._rport:
1991
      self._AssembleNet(minor,
1992
                        (self._lhost, self._lport, self._rhost, self._rport),
1993
                        constants.DRBD_NET_PROTOCOL,
1994
                        hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1995
    self._SetFromMinor(minor)
1996

    
1997
  @classmethod
1998
  def _ShutdownLocal(cls, minor):
1999
    """Detach from the local device.
2000

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

2004
    """
2005
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "detach"])
2006
    if result.failed:
2007
      _ThrowError("drbd%d: can't detach local disk: %s", minor, result.output)
2008

    
2009
  @classmethod
2010
  def _ShutdownNet(cls, minor):
2011
    """Disconnect from the remote peer.
2012

2013
    This fails if we don't have a local device.
2014

2015
    """
2016
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disconnect"])
2017
    if result.failed:
2018
      _ThrowError("drbd%d: can't shutdown network: %s", minor, result.output)
2019

    
2020
  @classmethod
2021
  def _ShutdownAll(cls, minor):
2022
    """Deactivate the device.
2023

2024
    This will, of course, fail if the device is in use.
2025

2026
    """
2027
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "down"])
2028
    if result.failed:
2029
      _ThrowError("drbd%d: can't shutdown drbd device: %s",
2030
                  minor, result.output)
2031

    
2032
  def Shutdown(self):
2033
    """Shutdown the DRBD device.
2034

2035
    """
2036
    if self.minor is None and not self.Attach():
2037
      logging.info("drbd%d: not attached during Shutdown()", self._aminor)
2038
      return
2039
    minor = self.minor
2040
    self.minor = None
2041
    self.dev_path = None
2042
    self._ShutdownAll(minor)
2043

    
2044
  def Remove(self):
2045
    """Stub remove for DRBD devices.
2046

2047
    """
2048
    self.Shutdown()
2049

    
2050
  @classmethod
2051
  def Create(cls, unique_id, children, size, params):
2052
    """Create a new DRBD8 device.
2053

2054
    Since DRBD devices are not created per se, just assembled, this
2055
    function only initializes the metadata.
2056

2057
    """
2058
    if len(children) != 2:
2059
      raise errors.ProgrammerError("Invalid setup for the drbd device")
2060
    # check that the minor is unused
2061
    aminor = unique_id[4]
2062
    proc_info = cls._MassageProcData(cls._GetProcData())
2063
    if aminor in proc_info:
2064
      status = DRBD8Status(proc_info[aminor])
2065
      in_use = status.is_in_use
2066
    else:
2067
      in_use = False
2068
    if in_use:
2069
      _ThrowError("drbd%d: minor is already in use at Create() time", aminor)
2070
    meta = children[1]
2071
    meta.Assemble()
2072
    if not meta.Attach():
2073
      _ThrowError("drbd%d: can't attach to meta device '%s'",
2074
                  aminor, meta)
2075
    cls._CheckMetaSize(meta.dev_path)
2076
    cls._InitMeta(aminor, meta.dev_path)
2077
    return cls(unique_id, children, size, params)
2078

    
2079
  def Grow(self, amount, dryrun, backingstore):
2080
    """Resize the DRBD device and its backing storage.
2081

2082
    """
2083
    if self.minor is None:
2084
      _ThrowError("drbd%d: Grow called while not attached", self._aminor)
2085
    if len(self._children) != 2 or None in self._children:
2086
      _ThrowError("drbd%d: cannot grow diskless device", self.minor)
2087
    self._children[0].Grow(amount, dryrun, backingstore)
2088
    if dryrun or backingstore:
2089
      # DRBD does not support dry-run mode and is not backing storage,
2090
      # so we'll return here
2091
      return
2092
    result = utils.RunCmd(["drbdsetup", self.dev_path, "resize", "-s",
2093
                           "%dm" % (self.size + amount)])
2094
    if result.failed:
2095
      _ThrowError("drbd%d: resize failed: %s", self.minor, result.output)
2096

    
2097

    
2098
class FileStorage(BlockDev):
2099
  """File device.
2100

2101
  This class represents the a file storage backend device.
2102

2103
  The unique_id for the file device is a (file_driver, file_path) tuple.
2104

2105
  """
2106
  def __init__(self, unique_id, children, size, params):
2107
    """Initalizes a file device backend.
2108

2109
    """
2110
    if children:
2111
      raise errors.BlockDeviceError("Invalid setup for file device")
2112
    super(FileStorage, self).__init__(unique_id, children, size, params)
2113
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
2114
      raise ValueError("Invalid configuration data %s" % str(unique_id))
2115
    self.driver = unique_id[0]
2116
    self.dev_path = unique_id[1]
2117
    self.Attach()
2118

    
2119
  def Assemble(self):
2120
    """Assemble the device.
2121

2122
    Checks whether the file device exists, raises BlockDeviceError otherwise.
2123

2124
    """
2125
    if not os.path.exists(self.dev_path):
2126
      _ThrowError("File device '%s' does not exist" % self.dev_path)
2127

    
2128
  def Shutdown(self):
2129
    """Shutdown the device.
2130

2131
    This is a no-op for the file type, as we don't deactivate
2132
    the file on shutdown.
2133

2134
    """
2135
    pass
2136

    
2137
  def Open(self, force=False):
2138
    """Make the device ready for I/O.
2139

2140
    This is a no-op for the file type.
2141

2142
    """
2143
    pass
2144

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

2148
    This is a no-op for the file type.
2149

2150
    """
2151
    pass
2152

    
2153
  def Remove(self):
2154
    """Remove the file backing the block device.
2155

2156
    @rtype: boolean
2157
    @return: True if the removal was successful
2158

2159
    """
2160
    try:
2161
      os.remove(self.dev_path)
2162
    except OSError, err:
2163
      if err.errno != errno.ENOENT:
2164
        _ThrowError("Can't remove file '%s': %s", self.dev_path, err)
2165

    
2166
  def Rename(self, new_id):
2167
    """Renames the file.
2168

2169
    """
2170
    # TODO: implement rename for file-based storage
2171
    _ThrowError("Rename is not supported for file-based storage")
2172

    
2173
  def Grow(self, amount, dryrun, backingstore):
2174
    """Grow the file
2175

2176
    @param amount: the amount (in mebibytes) to grow with
2177

2178
    """
2179
    if not backingstore:
2180
      return
2181
    # Check that the file exists
2182
    self.Assemble()
2183
    current_size = self.GetActualSize()
2184
    new_size = current_size + amount * 1024 * 1024
2185
    assert new_size > current_size, "Cannot Grow with a negative amount"
2186
    # We can't really simulate the growth
2187
    if dryrun:
2188
      return
2189
    try:
2190
      f = open(self.dev_path, "a+")
2191
      f.truncate(new_size)
2192
      f.close()
2193
    except EnvironmentError, err:
2194
      _ThrowError("Error in file growth: %", str(err))
2195

    
2196
  def Attach(self):
2197
    """Attach to an existing file.
2198

2199
    Check if this file already exists.
2200

2201
    @rtype: boolean
2202
    @return: True if file exists
2203

2204
    """
2205
    self.attached = os.path.exists(self.dev_path)
2206
    return self.attached
2207

    
2208
  def GetActualSize(self):
2209
    """Return the actual disk size.
2210

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

2213
    """
2214
    assert self.attached, "BlockDevice not attached in GetActualSize()"
2215
    try:
2216
      st = os.stat(self.dev_path)
2217
      return st.st_size
2218
    except OSError, err:
2219
      _ThrowError("Can't stat %s: %s", self.dev_path, err)
2220

    
2221
  @classmethod
2222
  def Create(cls, unique_id, children, size, params):
2223
    """Create a new file.
2224

2225
    @param size: the size of file in MiB
2226

2227
    @rtype: L{bdev.FileStorage}
2228
    @return: an instance of FileStorage
2229

2230
    """
2231
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
2232
      raise ValueError("Invalid configuration data %s" % str(unique_id))
2233
    dev_path = unique_id[1]
2234
    try:
2235
      fd = os.open(dev_path, os.O_RDWR | os.O_CREAT | os.O_EXCL)
2236
      f = os.fdopen(fd, "w")
2237
      f.truncate(size * 1024 * 1024)
2238
      f.close()
2239
    except EnvironmentError, err:
2240
      if err.errno == errno.EEXIST:
2241
        _ThrowError("File already existing: %s", dev_path)
2242
      _ThrowError("Error in file creation: %", str(err))
2243

    
2244
    return FileStorage(unique_id, children, size, params)
2245

    
2246

    
2247
class PersistentBlockDevice(BlockDev):
2248
  """A block device with persistent node
2249

2250
  May be either directly attached, or exposed through DM (e.g. dm-multipath).
2251
  udev helpers are probably required to give persistent, human-friendly
2252
  names.
2253

2254
  For the time being, pathnames are required to lie under /dev.
2255

2256
  """
2257
  def __init__(self, unique_id, children, size, params):
2258
    """Attaches to a static block device.
2259

2260
    The unique_id is a path under /dev.
2261

2262
    """
2263
    super(PersistentBlockDevice, self).__init__(unique_id, children, size,
2264
                                                params)
2265
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
2266
      raise ValueError("Invalid configuration data %s" % str(unique_id))
2267
    self.dev_path = unique_id[1]
2268
    if not os.path.realpath(self.dev_path).startswith("/dev/"):
2269
      raise ValueError("Full path '%s' lies outside /dev" %
2270
                              os.path.realpath(self.dev_path))
2271
    # TODO: this is just a safety guard checking that we only deal with devices
2272
    # we know how to handle. In the future this will be integrated with
2273
    # external storage backends and possible values will probably be collected
2274
    # from the cluster configuration.
2275
    if unique_id[0] != constants.BLOCKDEV_DRIVER_MANUAL:
2276
      raise ValueError("Got persistent block device of invalid type: %s" %
2277
                       unique_id[0])
2278

    
2279
    self.major = self.minor = None
2280
    self.Attach()
2281

    
2282
  @classmethod
2283
  def Create(cls, unique_id, children, size, params):
2284
    """Create a new device
2285

2286
    This is a noop, we only return a PersistentBlockDevice instance
2287

2288
    """
2289
    return PersistentBlockDevice(unique_id, children, 0, params)
2290

    
2291
  def Remove(self):
2292
    """Remove a device
2293

2294
    This is a noop
2295

2296
    """
2297
    pass
2298

    
2299
  def Rename(self, new_id):
2300
    """Rename this device.
2301

2302
    """
2303
    _ThrowError("Rename is not supported for PersistentBlockDev storage")
2304

    
2305
  def Attach(self):
2306
    """Attach to an existing block device.
2307

2308

2309
    """
2310
    self.attached = False
2311
    try:
2312
      st = os.stat(self.dev_path)
2313
    except OSError, err:
2314
      logging.error("Error stat()'ing %s: %s", self.dev_path, str(err))
2315
      return False
2316

    
2317
    if not stat.S_ISBLK(st.st_mode):
2318
      logging.error("%s is not a block device", self.dev_path)
2319
      return False
2320

    
2321
    self.major = os.major(st.st_rdev)
2322
    self.minor = os.minor(st.st_rdev)
2323
    self.attached = True
2324

    
2325
    return True
2326

    
2327
  def Assemble(self):
2328
    """Assemble the device.
2329

2330
    """
2331
    pass
2332

    
2333
  def Shutdown(self):
2334
    """Shutdown the device.
2335

2336
    """
2337
    pass
2338

    
2339
  def Open(self, force=False):
2340
    """Make the device ready for I/O.
2341

2342
    """
2343
    pass
2344

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

2348
    """
2349
    pass
2350

    
2351
  def Grow(self, amount, dryrun, backingstore):
2352
    """Grow the logical volume.
2353

2354
    """
2355
    _ThrowError("Grow is not supported for PersistentBlockDev storage")
2356

    
2357

    
2358
class RADOSBlockDevice(BlockDev):
2359
  """A RADOS Block Device (rbd).
2360

2361
  This class implements the RADOS Block Device for the backend. You need
2362
  the rbd kernel driver, the RADOS Tools and a working RADOS cluster for
2363
  this to be functional.
2364

2365
  """
2366
  def __init__(self, unique_id, children, size, params):
2367
    """Attaches to an rbd device.
2368

2369
    """
2370
    super(RADOSBlockDevice, self).__init__(unique_id, children, size, params)
2371
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
2372
      raise ValueError("Invalid configuration data %s" % str(unique_id))
2373

    
2374
    self.driver, self.rbd_name = unique_id
2375

    
2376
    self.major = self.minor = None
2377
    self.Attach()
2378

    
2379
  @classmethod
2380
  def Create(cls, unique_id, children, size, params):
2381
    """Create a new rbd device.
2382

2383
    Provision a new rbd volume inside a RADOS pool.
2384

2385
    """
2386
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
2387
      raise errors.ProgrammerError("Invalid configuration data %s" %
2388
                                   str(unique_id))
2389
    rbd_pool = params[constants.LDP_POOL]
2390
    rbd_name = unique_id[1]
2391

    
2392
    # Provision a new rbd volume (Image) inside the RADOS cluster.
2393
    cmd = [constants.RBD_CMD, "create", "-p", rbd_pool,
2394
           rbd_name, "--size", "%s" % size]
2395
    result = utils.RunCmd(cmd)
2396
    if result.failed:
2397
      _ThrowError("rbd creation failed (%s): %s",
2398
                  result.fail_reason, result.output)
2399

    
2400
    return RADOSBlockDevice(unique_id, children, size, params)
2401

    
2402
  def Remove(self):
2403
    """Remove the rbd device.
2404

2405
    """
2406
    rbd_pool = self.params[constants.LDP_POOL]
2407
    rbd_name = self.unique_id[1]
2408

    
2409
    if not self.minor and not self.Attach():
2410
      # The rbd device doesn't exist.
2411
      return
2412

    
2413
    # First shutdown the device (remove mappings).
2414
    self.Shutdown()
2415

    
2416
    # Remove the actual Volume (Image) from the RADOS cluster.
2417
    cmd = [constants.RBD_CMD, "rm", "-p", rbd_pool, rbd_name]
2418
    result = utils.RunCmd(cmd)
2419
    if result.failed:
2420
      _ThrowError("Can't remove Volume from cluster with rbd rm: %s - %s",
2421
                  result.fail_reason, result.output)
2422

    
2423
  def Rename(self, new_id):
2424
    """Rename this device.
2425

2426
    """
2427
    pass
2428

    
2429
  def Attach(self):
2430
    """Attach to an existing rbd device.
2431

2432
    This method maps the rbd volume that matches our name with
2433
    an rbd device and then attaches to this device.
2434

2435
    """
2436
    self.attached = False
2437

    
2438
    # Map the rbd volume to a block device under /dev
2439
    self.dev_path = self._MapVolumeToBlockdev(self.unique_id)
2440

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

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

    
2451
    self.major = os.major(st.st_rdev)
2452
    self.minor = os.minor(st.st_rdev)
2453
    self.attached = True
2454

    
2455
    return True
2456

    
2457
  def _MapVolumeToBlockdev(self, unique_id):
2458
    """Maps existing rbd volumes to block devices.
2459

2460
    This method should be idempotent if the mapping already exists.
2461

2462
    @rtype: string
2463
    @return: the block device path that corresponds to the volume
2464

2465
    """
2466
    pool = self.params[constants.LDP_POOL]
2467
    name = unique_id[1]
2468

    
2469
    # Check if the mapping already exists.
2470
    showmap_cmd = [constants.RBD_CMD, "showmapped", "-p", pool]
2471
    result = utils.RunCmd(showmap_cmd)
2472
    if result.failed:
2473
      _ThrowError("rbd showmapped failed (%s): %s",
2474
                  result.fail_reason, result.output)
2475

    
2476
    rbd_dev = self._ParseRbdShowmappedOutput(result.output, name)
2477

    
2478
    if rbd_dev:
2479
      # The mapping exists. Return it.
2480
      return rbd_dev
2481

    
2482
    # The mapping doesn't exist. Create it.
2483
    map_cmd = [constants.RBD_CMD, "map", "-p", pool, name]
2484
    result = utils.RunCmd(map_cmd)
2485
    if result.failed:
2486
      _ThrowError("rbd map failed (%s): %s",
2487
                  result.fail_reason, result.output)
2488

    
2489
    # Find the corresponding rbd device.
2490
    showmap_cmd = [constants.RBD_CMD, "showmapped", "-p", pool]
2491
    result = utils.RunCmd(showmap_cmd)
2492
    if result.failed:
2493
      _ThrowError("rbd map succeeded, but showmapped failed (%s): %s",
2494
                  result.fail_reason, result.output)
2495

    
2496
    rbd_dev = self._ParseRbdShowmappedOutput(result.output, name)
2497

    
2498
    if not rbd_dev:
2499
      _ThrowError("rbd map succeeded, but could not find the rbd block"
2500
                  " device in output of showmapped, for volume: %s", name)
2501

    
2502
    # The device was successfully mapped. Return it.
2503
    return rbd_dev
2504

    
2505
  @staticmethod
2506
  def _ParseRbdShowmappedOutput(output, volume_name):
2507
    """Parse the output of `rbd showmapped'.
2508

2509
    This method parses the output of `rbd showmapped' and returns
2510
    the rbd block device path (e.g. /dev/rbd0) that matches the
2511
    given rbd volume.
2512

2513
    @type output: string
2514
    @param output: the whole output of `rbd showmapped'
2515
    @type volume_name: string
2516
    @param volume_name: the name of the volume whose device we search for
2517
    @rtype: string or None
2518
    @return: block device path if the volume is mapped, else None
2519

2520
    """
2521
    allfields = 5
2522
    volumefield = 2
2523
    devicefield = 4
2524

    
2525
    field_sep = "\t"
2526

    
2527
    lines = output.splitlines()
2528
    splitted_lines = map(lambda l: l.split(field_sep), lines)
2529

    
2530
    # Check empty output.
2531
    if not splitted_lines:
2532
      _ThrowError("rbd showmapped returned empty output")
2533

    
2534
    # Check showmapped header line, to determine number of fields.
2535
    field_cnt = len(splitted_lines[0])
2536
    if field_cnt != allfields:
2537
      _ThrowError("Cannot parse rbd showmapped output because its format"
2538
                  " seems to have changed; expected %s fields, found %s",
2539
                  allfields, field_cnt)
2540

    
2541
    matched_lines = \
2542
      filter(lambda l: len(l) == allfields and l[volumefield] == volume_name,
2543
             splitted_lines)
2544

    
2545
    if len(matched_lines) > 1:
2546
      _ThrowError("The rbd volume %s is mapped more than once."
2547
                  " This shouldn't happen, try to unmap the extra"
2548
                  " devices manually.", volume_name)
2549

    
2550
    if matched_lines:
2551
      # rbd block device found. Return it.
2552
      rbd_dev = matched_lines[0][devicefield]
2553
      return rbd_dev
2554

    
2555
    # The given volume is not mapped.
2556
    return None
2557

    
2558
  def Assemble(self):
2559
    """Assemble the device.
2560

2561
    """
2562
    pass
2563

    
2564
  def Shutdown(self):
2565
    """Shutdown the device.
2566

2567
    """
2568
    if not self.minor and not self.Attach():
2569
      # The rbd device doesn't exist.
2570
      return
2571

    
2572
    # Unmap the block device from the Volume.
2573
    self._UnmapVolumeFromBlockdev(self.unique_id)
2574

    
2575
    self.minor = None
2576
    self.dev_path = None
2577

    
2578
  def _UnmapVolumeFromBlockdev(self, unique_id):
2579
    """Unmaps the rbd device from the Volume it is mapped.
2580

2581
    Unmaps the rbd device from the Volume it was previously mapped to.
2582
    This method should be idempotent if the Volume isn't mapped.
2583

2584
    """
2585
    pool = self.params[constants.LDP_POOL]
2586
    name = unique_id[1]
2587

    
2588
    # Check if the mapping already exists.
2589
    showmap_cmd = [constants.RBD_CMD, "showmapped", "-p", pool]
2590
    result = utils.RunCmd(showmap_cmd)
2591
    if result.failed:
2592
      _ThrowError("rbd showmapped failed [during unmap](%s): %s",
2593
                  result.fail_reason, result.output)
2594

    
2595
    rbd_dev = self._ParseRbdShowmappedOutput(result.output, name)
2596

    
2597
    if rbd_dev:
2598
      # The mapping exists. Unmap the rbd device.
2599
      unmap_cmd = [constants.RBD_CMD, "unmap", "%s" % rbd_dev]
2600
      result = utils.RunCmd(unmap_cmd)
2601
      if result.failed:
2602
        _ThrowError("rbd unmap failed (%s): %s",
2603
                    result.fail_reason, result.output)
2604

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

2608
    """
2609
    pass
2610

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

2614
    """
2615
    pass
2616

    
2617
  def Grow(self, amount, dryrun, backingstore):
2618
    """Grow the Volume.
2619

2620
    @type amount: integer
2621
    @param amount: the amount (in mebibytes) to grow with
2622
    @type dryrun: boolean
2623
    @param dryrun: whether to execute the operation in simulation mode
2624
        only, without actually increasing the size
2625

2626
    """
2627
    if not backingstore:
2628
      return
2629
    if not self.Attach():
2630
      _ThrowError("Can't attach to rbd device during Grow()")
2631

    
2632
    if dryrun:
2633
      # the rbd tool does not support dry runs of resize operations.
2634
      # Since rbd volumes are thinly provisioned, we assume
2635
      # there is always enough free space for the operation.
2636
      return
2637

    
2638
    rbd_pool = self.params[constants.LDP_POOL]
2639
    rbd_name = self.unique_id[1]
2640
    new_size = self.size + amount
2641

    
2642
    # Resize the rbd volume (Image) inside the RADOS cluster.
2643
    cmd = [constants.RBD_CMD, "resize", "-p", rbd_pool,
2644
           rbd_name, "--size", "%s" % new_size]
2645
    result = utils.RunCmd(cmd)
2646
    if result.failed:
2647
      _ThrowError("rbd resize failed (%s): %s",
2648
                  result.fail_reason, result.output)
2649

    
2650

    
2651
DEV_MAP = {
2652
  constants.LD_LV: LogicalVolume,
2653
  constants.LD_DRBD8: DRBD8,
2654
  constants.LD_BLOCKDEV: PersistentBlockDevice,
2655
  constants.LD_RBD: RADOSBlockDevice,
2656
  }
2657

    
2658
if constants.ENABLE_FILE_STORAGE or constants.ENABLE_SHARED_FILE_STORAGE:
2659
  DEV_MAP[constants.LD_FILE] = FileStorage
2660

    
2661

    
2662
def _VerifyDiskType(dev_type):
2663
  if dev_type not in DEV_MAP:
2664
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
2665

    
2666

    
2667
def _VerifyDiskParams(disk):
2668
  """Verifies if all disk parameters are set.
2669

2670
  """
2671
  missing = set(constants.DISK_LD_DEFAULTS[disk.dev_type]) - set(disk.params)
2672
  if missing:
2673
    raise errors.ProgrammerError("Block device is missing disk parameters: %s" %
2674
                                 missing)
2675

    
2676

    
2677
def FindDevice(disk, children):
2678
  """Search for an existing, assembled device.
2679

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

2683
  @type disk: L{objects.Disk}
2684
  @param disk: the disk object to find
2685
  @type children: list of L{bdev.BlockDev}
2686
  @param children: the list of block devices that are children of the device
2687
                  represented by the disk parameter
2688

2689
  """
2690
  _VerifyDiskType(disk.dev_type)
2691
  device = DEV_MAP[disk.dev_type](disk.physical_id, children, disk.size,
2692
                                  disk.params)
2693
  if not device.attached:
2694
    return None
2695
  return device
2696

    
2697

    
2698
def Assemble(disk, children):
2699
  """Try to attach or assemble an existing device.
2700

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

2704
  @type disk: L{objects.Disk}
2705
  @param disk: the disk object to assemble
2706
  @type children: list of L{bdev.BlockDev}
2707
  @param children: the list of block devices that are children of the device
2708
                  represented by the disk parameter
2709

2710
  """
2711
  _VerifyDiskType(disk.dev_type)
2712
  _VerifyDiskParams(disk)
2713
  device = DEV_MAP[disk.dev_type](disk.physical_id, children, disk.size,
2714
                                  disk.params)
2715
  device.Assemble()
2716
  return device
2717

    
2718

    
2719
def Create(disk, children):
2720
  """Create a device.
2721

2722
  @type disk: L{objects.Disk}
2723
  @param disk: the disk object to create
2724
  @type children: list of L{bdev.BlockDev}
2725
  @param children: the list of block devices that are children of the device
2726
                  represented by the disk parameter
2727

2728
  """
2729
  _VerifyDiskType(disk.dev_type)
2730
  _VerifyDiskParams(disk)
2731
  device = DEV_MAP[disk.dev_type].Create(disk.physical_id, children, disk.size,
2732
                                         disk.params)
2733
  return device