Statistics
| Branch: | Tag: | Revision:

root / lib / bdev.py @ e687ec01

History | View | Annotate | Download (71.5 kB)

1
#
2
#
3

    
4
# Copyright (C) 2006, 2007, 2010, 2011 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 stat
28
import pyparsing as pyp
29
import os
30
import logging
31

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

    
39

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

    
43

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

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

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

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

    
61

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

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

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

    
75

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

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

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

    
89

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

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

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

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

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

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

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

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

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

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

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

153
    """
154
    pass
155

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

159
    """
160
    raise NotImplementedError
161

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

165
    """
166
    raise NotImplementedError
167

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

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

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

179
    """
180
    raise NotImplementedError
181

    
182
  def Remove(self):
183
    """Remove this device.
184

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

189
    """
190
    raise NotImplementedError
191

    
192
  def Rename(self, new_id):
193
    """Rename this device.
194

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

197
    """
198
    raise NotImplementedError
199

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

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

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

209
    """
210
    raise NotImplementedError
211

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

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

219
    """
220
    raise NotImplementedError
221

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

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

227
    """
228
    result = True
229
    if self._children:
230
      for child in self._children:
231
        result = result and child.SetSyncSpeed(speed)
232
    return result
233

    
234
  def PauseResumeSync(self, pause):
235
    """Pause/Resume the sync of the mirror.
236

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

239
    @param pause: Wheater to pause or resume
240

241
    """
242
    result = True
243
    if self._children:
244
      for child in self._children:
245
        result = result and child.PauseResumeSync(pause)
246
    return result
247

    
248
  def GetSyncStatus(self):
249
    """Returns the sync status of the device.
250

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

254
    If sync_percent is None, it means the device is not syncing.
255

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

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

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

267
    @rtype: objects.BlockDevStatus
268

269
    """
270
    return objects.BlockDevStatus(dev_path=self.dev_path,
271
                                  major=self.major,
272
                                  minor=self.minor,
273
                                  sync_percent=None,
274
                                  estimated_time=None,
275
                                  is_degraded=False,
276
                                  ldisk_status=constants.LDS_OKAY)
277

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

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

285
    @rtype: objects.BlockDevStatus
286

287
    """
288
    status = self.GetSyncStatus()
289

    
290
    min_percent = status.sync_percent
291
    max_time = status.estimated_time
292
    is_degraded = status.is_degraded
293
    ldisk_status = status.ldisk_status
294

    
295
    if self._children:
296
      for child in self._children:
297
        child_status = child.GetSyncStatus()
298

    
299
        if min_percent is None:
300
          min_percent = child_status.sync_percent
301
        elif child_status.sync_percent is not None:
302
          min_percent = min(min_percent, child_status.sync_percent)
303

    
304
        if max_time is None:
305
          max_time = child_status.estimated_time
306
        elif child_status.estimated_time is not None:
307
          max_time = max(max_time, child_status.estimated_time)
308

    
309
        is_degraded = is_degraded or child_status.is_degraded
310

    
311
        if ldisk_status is None:
312
          ldisk_status = child_status.ldisk_status
313
        elif child_status.ldisk_status is not None:
314
          ldisk_status = max(ldisk_status, child_status.ldisk_status)
315

    
316
    return objects.BlockDevStatus(dev_path=self.dev_path,
317
                                  major=self.major,
318
                                  minor=self.minor,
319
                                  sync_percent=min_percent,
320
                                  estimated_time=max_time,
321
                                  is_degraded=is_degraded,
322
                                  ldisk_status=ldisk_status)
323

    
324
  def SetInfo(self, text):
325
    """Update metadata with info text.
326

327
    Only supported for some device types.
328

329
    """
330
    for child in self._children:
331
      child.SetInfo(text)
332

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

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

342
    """
343
    raise NotImplementedError
344

    
345
  def GetActualSize(self):
346
    """Return the actual disk size.
347

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

350
    """
351
    assert self.attached, "BlockDevice not attached in GetActualSize()"
352
    result = utils.RunCmd(["blockdev", "--getsize64", self.dev_path])
353
    if result.failed:
354
      _ThrowError("blockdev failed (%s): %s",
355
                  result.fail_reason, result.output)
356
    try:
357
      sz = int(result.output.strip())
358
    except (ValueError, TypeError), err:
359
      _ThrowError("Failed to parse blockdev output: %s", str(err))
360
    return sz
361

    
362
  def __repr__(self):
363
    return ("<%s: unique_id: %s, children: %s, %s:%s, %s>" %
364
            (self.__class__, self.unique_id, self._children,
365
             self.major, self.minor, self.dev_path))
366

    
367

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

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

    
376
  def __init__(self, unique_id, children, size):
377
    """Attaches to a LV device.
378

379
    The unique_id is a tuple (vg_name, lv_name)
380

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

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

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

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

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

    
438
  @staticmethod
439
  def _GetVolumeInfo(lvm_cmd, fields):
440
    """Returns LVM Volumen infos using lvm_cmd
441

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

446
    """
447
    if not fields:
448
      raise errors.ProgrammerError("No fields specified")
449

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

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

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

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

    
467
      data.append(splitted_fields)
468

    
469
    return data
470

    
471
  @classmethod
472
  def GetPVInfo(cls, vg_names, filter_allocatable=True):
473
    """Get the free space info for PVs in a volume group.
474

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

478
    @rtype: list
479
    @return: list of tuples (free_space, name) with free_space in mebibytes
480

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

    
489
    data = []
490
    for pv_name, vg_name, pv_free, pv_attr in info:
491
      # (possibly) skip over pvs which are not allocatable
492
      if filter_allocatable and pv_attr[0] != "a":
493
        continue
494
      # (possibly) skip over pvs which are not in the right volume group(s)
495
      if vg_names and vg_name not in vg_names:
496
        continue
497
      data.append((float(pv_free), pv_name, vg_name))
498

    
499
    return data
500

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

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

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

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

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

    
530
    return data
531

    
532
  @classmethod
533
  def _ValidateName(cls, name):
534
    """Validates that a given name is valid as VG or LV name.
535

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

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

    
546
  def Remove(self):
547
    """Remove this logical volume.
548

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

    
558
  def Rename(self, new_id):
559
    """Rename this logical volume.
560

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

    
575
  def Attach(self):
576
    """Attach to an existing LV.
577

578
    This method will try to see if an existing and active LV exists
579
    which matches our name. If so, its major/minor will be
580
    recorded.
581

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

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

    
613
    try:
614
      major = int(major)
615
      minor = int(minor)
616
    except (TypeError, ValueError), err:
617
      logging.error("lvs major/minor cannot be parsed: %s", str(err))
618

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

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

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

    
640
  def Assemble(self):
641
    """Assemble the device.
642

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

647
    """
648
    result = utils.RunCmd(["lvchange", "-ay", self.dev_path])
649
    if result.failed:
650
      _ThrowError("Can't activate lv %s: %s", self.dev_path, result.output)
651

    
652
  def Shutdown(self):
653
    """Shutdown the device.
654

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

658
    """
659
    pass
660

    
661
  def GetSyncStatus(self):
662
    """Returns the sync status of the device.
663

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

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

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

678
    The status was already read in Attach, so we just return it.
679

680
    @rtype: objects.BlockDevStatus
681

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

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

    
696
  def Open(self, force=False):
697
    """Make the device ready for I/O.
698

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

701
    """
702
    pass
703

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

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

709
    """
710
    pass
711

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

715
    @returns: tuple (vg, lv)
716

717
    """
718
    snap_name = self._lv_name + ".snap"
719

    
720
    # remove existing snapshot if found
721
    snap = LogicalVolume((self._vg_name, snap_name), None, size)
722
    _IgnoreError(snap.Remove)
723

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

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

    
738
    return (self._vg_name, snap_name)
739

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

743
    """
744
    BlockDev.SetInfo(self, text)
745

    
746
    # Replace invalid characters
747
    text = re.sub("^[^A-Za-z0-9_+.]", "_", text)
748
    text = re.sub("[^-A-Za-z0-9_+.]", "_", text)
749

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

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

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

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

    
783

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

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

789
  """
790
  UNCONF_RE = re.compile(r"\s*[0-9]+:\s*cs:Unconfigured$")
791
  LINE_RE = re.compile(r"\s*[0-9]+:\s*cs:(\S+)\s+(?:st|ro):([^/]+)/(\S+)"
792
                       "\s+ds:([^/]+)/(\S+)\s+.*$")
793
  SYNC_RE = re.compile(r"^.*\ssync'ed:\s*([0-9.]+)%.*"
794
                       "\sfinish: ([0-9]+):([0-9]+):([0-9]+)\s.*$")
795

    
796
  CS_UNCONFIGURED = "Unconfigured"
797
  CS_STANDALONE = "StandAlone"
798
  CS_WFCONNECTION = "WFConnection"
799
  CS_WFREPORTPARAMS = "WFReportParams"
800
  CS_CONNECTED = "Connected"
801
  CS_STARTINGSYNCS = "StartingSyncS"
802
  CS_STARTINGSYNCT = "StartingSyncT"
803
  CS_WFBITMAPS = "WFBitMapS"
804
  CS_WFBITMAPT = "WFBitMapT"
805
  CS_WFSYNCUUID = "WFSyncUUID"
806
  CS_SYNCSOURCE = "SyncSource"
807
  CS_SYNCTARGET = "SyncTarget"
808
  CS_PAUSEDSYNCS = "PausedSyncS"
809
  CS_PAUSEDSYNCT = "PausedSyncT"
810
  CSET_SYNC = frozenset([
811
    CS_WFREPORTPARAMS,
812
    CS_STARTINGSYNCS,
813
    CS_STARTINGSYNCT,
814
    CS_WFBITMAPS,
815
    CS_WFBITMAPT,
816
    CS_WFSYNCUUID,
817
    CS_SYNCSOURCE,
818
    CS_SYNCTARGET,
819
    CS_PAUSEDSYNCS,
820
    CS_PAUSEDSYNCT,
821
    ])
822

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

    
833
  RO_PRIMARY = "Primary"
834
  RO_SECONDARY = "Secondary"
835
  RO_UNKNOWN = "Unknown"
836

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

    
852
    # end reading of data from the LINE_RE or UNCONF_RE
853

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

    
864
    self.is_diskless = self.ldisk == self.DS_DISKLESS
865
    self.is_disk_uptodate = self.ldisk == self.DS_UPTODATE
866

    
867
    self.is_in_resync = self.cstatus in self.CSET_SYNC
868
    self.is_in_use = self.cstatus != self.CS_UNCONFIGURED
869

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

    
888

    
889
class BaseDRBD(BlockDev): # pylint: disable-msg=W0223
890
  """Base DRBD class.
891

892
  This class contains a few bits of common functionality between the
893
  0.7 and 8.x versions of DRBD.
894

895
  """
896
  _VERSION_RE = re.compile(r"^version: (\d+)\.(\d+)\.(\d+)(?:\.\d+)?"
897
                           r" \(api:(\d+)/proto:(\d+)(?:-(\d+))?\)")
898
  _VALID_LINE_RE = re.compile("^ *([0-9]+): cs:([^ ]+).*$")
899
  _UNUSED_LINE_RE = re.compile("^ *([0-9]+): cs:Unconfigured$")
900

    
901
  _DRBD_MAJOR = 147
902
  _ST_UNCONFIGURED = "Unconfigured"
903
  _ST_WFCONNECTION = "WFConnection"
904
  _ST_CONNECTED = "Connected"
905

    
906
  _STATUS_FILE = "/proc/drbd"
907
  _USERMODE_HELPER_FILE = "/sys/module/drbd/parameters/usermode_helper"
908

    
909
  @staticmethod
910
  def _GetProcData(filename=_STATUS_FILE):
911
    """Return data from /proc/drbd.
912

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

    
926
  @classmethod
927
  def _MassageProcData(cls, data):
928
    """Transform the output of _GetProdData into a nicer form.
929

930
    @return: a dictionary of minor: joined lines from /proc/drbd
931
        for that minor
932

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

    
953
  @classmethod
954
  def _GetVersion(cls, proc_data):
955
    """Return the DRBD version.
956

957
    This will return a dict with keys:
958
      - k_major
959
      - k_minor
960
      - k_point
961
      - api
962
      - proto
963
      - proto2 (only on drbd > 8.2.X)
964

965
    """
966
    first_line = proc_data[0].strip()
967
    version = cls._VERSION_RE.match(first_line)
968
    if not version:
969
      raise errors.BlockDeviceError("Can't parse DRBD version from '%s'" %
970
                                    first_line)
971

    
972
    values = version.groups()
973
    retval = {"k_major": int(values[0]),
974
              "k_minor": int(values[1]),
975
              "k_point": int(values[2]),
976
              "api": int(values[3]),
977
              "proto": int(values[4]),
978
             }
979
    if values[5] is not None:
980
      retval["proto2"] = values[5]
981

    
982
    return retval
983

    
984
  @staticmethod
985
  def GetUsermodeHelper(filename=_USERMODE_HELPER_FILE):
986
    """Returns DRBD usermode_helper currently set.
987

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

    
1001
  @staticmethod
1002
  def _DevPath(minor):
1003
    """Return the path to a drbd device for a given minor.
1004

1005
    """
1006
    return "/dev/drbd%d" % minor
1007

    
1008
  @classmethod
1009
  def GetUsedDevs(cls):
1010
    """Compute the list of used DRBD devices.
1011

1012
    """
1013
    data = cls._GetProcData()
1014

    
1015
    used_devs = {}
1016
    for line in data:
1017
      match = cls._VALID_LINE_RE.match(line)
1018
      if not match:
1019
        continue
1020
      minor = int(match.group(1))
1021
      state = match.group(2)
1022
      if state == cls._ST_UNCONFIGURED:
1023
        continue
1024
      used_devs[minor] = state, line
1025

    
1026
    return used_devs
1027

    
1028
  def _SetFromMinor(self, minor):
1029
    """Set our parameters based on the given minor.
1030

1031
    This sets our minor variable and our dev_path.
1032

1033
    """
1034
    if minor is None:
1035
      self.minor = self.dev_path = None
1036
      self.attached = False
1037
    else:
1038
      self.minor = minor
1039
      self.dev_path = self._DevPath(minor)
1040
      self.attached = True
1041

    
1042
  @staticmethod
1043
  def _CheckMetaSize(meta_device):
1044
    """Check if the given meta device looks like a valid one.
1045

1046
    This currently only check the size, which must be around
1047
    128MiB.
1048

1049
    """
1050
    result = utils.RunCmd(["blockdev", "--getsize", meta_device])
1051
    if result.failed:
1052
      _ThrowError("Failed to get device size: %s - %s",
1053
                  result.fail_reason, result.output)
1054
    try:
1055
      sectors = int(result.stdout)
1056
    except (TypeError, ValueError):
1057
      _ThrowError("Invalid output from blockdev: '%s'", result.stdout)
1058
    num_bytes = sectors * 512
1059
    if num_bytes < 128 * 1024 * 1024: # less than 128MiB
1060
      _ThrowError("Meta device too small (%.2fMib)", (num_bytes / 1024 / 1024))
1061
    # the maximum *valid* size of the meta device when living on top
1062
    # of LVM is hard to compute: it depends on the number of stripes
1063
    # and the PE size; e.g. a 2-stripe, 64MB PE will result in a 128MB
1064
    # (normal size), but an eight-stripe 128MB PE will result in a 1GB
1065
    # size meta device; as such, we restrict it to 1GB (a little bit
1066
    # too generous, but making assumptions about PE size is hard)
1067
    if num_bytes > 1024 * 1024 * 1024:
1068
      _ThrowError("Meta device too big (%.2fMiB)", (num_bytes / 1024 / 1024))
1069

    
1070
  def Rename(self, new_id):
1071
    """Rename a device.
1072

1073
    This is not supported for drbd devices.
1074

1075
    """
1076
    raise errors.ProgrammerError("Can't rename a drbd device")
1077

    
1078

    
1079
class DRBD8(BaseDRBD):
1080
  """DRBD v8.x block device.
1081

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

1086
  The unique_id for the drbd device is the (local_ip, local_port,
1087
  remote_ip, remote_port) tuple, and it must have two children: the
1088
  data device and the meta_device. The meta device is checked for
1089
  valid size and is zeroed on create.
1090

1091
  """
1092
  _MAX_MINORS = 255
1093
  _PARSE_SHOW = None
1094

    
1095
  # timeout constants
1096
  _NET_RECONFIG_TIMEOUT = 60
1097

    
1098
  def __init__(self, unique_id, children, size):
1099
    if children and children.count(None) > 0:
1100
      children = []
1101
    if len(children) not in (0, 2):
1102
      raise ValueError("Invalid configuration data %s" % str(children))
1103
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 6:
1104
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1105
    (self._lhost, self._lport,
1106
     self._rhost, self._rport,
1107
     self._aminor, self._secret) = unique_id
1108
    if children:
1109
      if not _CanReadDevice(children[1].dev_path):
1110
        logging.info("drbd%s: Ignoring unreadable meta device", self._aminor)
1111
        children = []
1112
    super(DRBD8, self).__init__(unique_id, children, size)
1113
    self.major = self._DRBD_MAJOR
1114
    version = self._GetVersion(self._GetProcData())
1115
    if version["k_major"] != 8:
1116
      _ThrowError("Mismatch in DRBD kernel version and requested ganeti"
1117
                  " usage: kernel is %s.%s, ganeti wants 8.x",
1118
                  version["k_major"], version["k_minor"])
1119

    
1120
    if (self._lhost is not None and self._lhost == self._rhost and
1121
        self._lport == self._rport):
1122
      raise ValueError("Invalid configuration data, same local/remote %s" %
1123
                       (unique_id,))
1124
    self.Attach()
1125

    
1126
  @classmethod
1127
  def _InitMeta(cls, minor, dev_path):
1128
    """Initialize a meta device.
1129

1130
    This will not work if the given minor is in use.
1131

1132
    """
1133
    # Zero the metadata first, in order to make sure drbdmeta doesn't
1134
    # try to auto-detect existing filesystems or similar (see
1135
    # http://code.google.com/p/ganeti/issues/detail?id=182); we only
1136
    # care about the first 128MB of data in the device, even though it
1137
    # can be bigger
1138
    result = utils.RunCmd([constants.DD_CMD,
1139
                           "if=/dev/zero", "of=%s" % dev_path,
1140
                           "bs=1048576", "count=128", "oflag=direct"])
1141
    if result.failed:
1142
      _ThrowError("Can't wipe the meta device: %s", result.output)
1143

    
1144
    result = utils.RunCmd(["drbdmeta", "--force", cls._DevPath(minor),
1145
                           "v08", dev_path, "0", "create-md"])
1146
    if result.failed:
1147
      _ThrowError("Can't initialize meta device: %s", result.output)
1148

    
1149
  @classmethod
1150
  def _FindUnusedMinor(cls):
1151
    """Find an unused DRBD device.
1152

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

1156
    """
1157
    data = cls._GetProcData()
1158

    
1159
    highest = None
1160
    for line in data:
1161
      match = cls._UNUSED_LINE_RE.match(line)
1162
      if match:
1163
        return int(match.group(1))
1164
      match = cls._VALID_LINE_RE.match(line)
1165
      if match:
1166
        minor = int(match.group(1))
1167
        highest = max(highest, minor)
1168
    if highest is None: # there are no minors in use at all
1169
      return 0
1170
    if highest >= cls._MAX_MINORS:
1171
      logging.error("Error: no free drbd minors!")
1172
      raise errors.BlockDeviceError("Can't find a free DRBD minor")
1173
    return highest + 1
1174

    
1175
  @classmethod
1176
  def _GetShowParser(cls):
1177
    """Return a parser for `drbd show` output.
1178

1179
    This will either create or return an already-create parser for the
1180
    output of the command `drbd show`.
1181

1182
    """
1183
    if cls._PARSE_SHOW is not None:
1184
      return cls._PARSE_SHOW
1185

    
1186
    # pyparsing setup
1187
    lbrace = pyp.Literal("{").suppress()
1188
    rbrace = pyp.Literal("}").suppress()
1189
    lbracket = pyp.Literal("[").suppress()
1190
    rbracket = pyp.Literal("]").suppress()
1191
    semi = pyp.Literal(";").suppress()
1192
    colon = pyp.Literal(":").suppress()
1193
    # this also converts the value to an int
1194
    number = pyp.Word(pyp.nums).setParseAction(lambda s, l, t: int(t[0]))
1195

    
1196
    comment = pyp.Literal("#") + pyp.Optional(pyp.restOfLine)
1197
    defa = pyp.Literal("_is_default").suppress()
1198
    dbl_quote = pyp.Literal('"').suppress()
1199

    
1200
    keyword = pyp.Word(pyp.alphanums + '-')
1201

    
1202
    # value types
1203
    value = pyp.Word(pyp.alphanums + '_-/.:')
1204
    quoted = dbl_quote + pyp.CharsNotIn('"') + dbl_quote
1205
    ipv4_addr = (pyp.Optional(pyp.Literal("ipv4")).suppress() +
1206
                 pyp.Word(pyp.nums + ".") + colon + number)
1207
    ipv6_addr = (pyp.Optional(pyp.Literal("ipv6")).suppress() +
1208
                 pyp.Optional(lbracket) + pyp.Word(pyp.hexnums + ":") +
1209
                 pyp.Optional(rbracket) + colon + number)
1210
    # meta device, extended syntax
1211
    meta_value = ((value ^ quoted) + lbracket + number + rbracket)
1212
    # device name, extended syntax
1213
    device_value = pyp.Literal("minor").suppress() + number
1214

    
1215
    # a statement
1216
    stmt = (~rbrace + keyword + ~lbrace +
1217
            pyp.Optional(ipv4_addr ^ ipv6_addr ^ value ^ quoted ^ meta_value ^
1218
                         device_value) +
1219
            pyp.Optional(defa) + semi +
1220
            pyp.Optional(pyp.restOfLine).suppress())
1221

    
1222
    # an entire section
1223
    section_name = pyp.Word(pyp.alphas + "_")
1224
    section = section_name + lbrace + pyp.ZeroOrMore(pyp.Group(stmt)) + rbrace
1225

    
1226
    bnf = pyp.ZeroOrMore(pyp.Group(section ^ stmt))
1227
    bnf.ignore(comment)
1228

    
1229
    cls._PARSE_SHOW = bnf
1230

    
1231
    return bnf
1232

    
1233
  @classmethod
1234
  def _GetShowData(cls, minor):
1235
    """Return the `drbdsetup show` data for a minor.
1236

1237
    """
1238
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "show"])
1239
    if result.failed:
1240
      logging.error("Can't display the drbd config: %s - %s",
1241
                    result.fail_reason, result.output)
1242
      return None
1243
    return result.stdout
1244

    
1245
  @classmethod
1246
  def _GetDevInfo(cls, out):
1247
    """Parse details about a given DRBD minor.
1248

1249
    This return, if available, the local backing device (as a path)
1250
    and the local and remote (ip, port) information from a string
1251
    containing the output of the `drbdsetup show` command as returned
1252
    by _GetShowData.
1253

1254
    """
1255
    data = {}
1256
    if not out:
1257
      return data
1258

    
1259
    bnf = cls._GetShowParser()
1260
    # run pyparse
1261

    
1262
    try:
1263
      results = bnf.parseString(out)
1264
    except pyp.ParseException, err:
1265
      _ThrowError("Can't parse drbdsetup show output: %s", str(err))
1266

    
1267
    # and massage the results into our desired format
1268
    for section in results:
1269
      sname = section[0]
1270
      if sname == "_this_host":
1271
        for lst in section[1:]:
1272
          if lst[0] == "disk":
1273
            data["local_dev"] = lst[1]
1274
          elif lst[0] == "meta-disk":
1275
            data["meta_dev"] = lst[1]
1276
            data["meta_index"] = lst[2]
1277
          elif lst[0] == "address":
1278
            data["local_addr"] = tuple(lst[1:])
1279
      elif sname == "_remote_host":
1280
        for lst in section[1:]:
1281
          if lst[0] == "address":
1282
            data["remote_addr"] = tuple(lst[1:])
1283
    return data
1284

    
1285
  def _MatchesLocal(self, info):
1286
    """Test if our local config matches with an existing device.
1287

1288
    The parameter should be as returned from `_GetDevInfo()`. This
1289
    method tests if our local backing device is the same as the one in
1290
    the info parameter, in effect testing if we look like the given
1291
    device.
1292

1293
    """
1294
    if self._children:
1295
      backend, meta = self._children
1296
    else:
1297
      backend = meta = None
1298

    
1299
    if backend is not None:
1300
      retval = ("local_dev" in info and info["local_dev"] == backend.dev_path)
1301
    else:
1302
      retval = ("local_dev" not in info)
1303

    
1304
    if meta is not None:
1305
      retval = retval and ("meta_dev" in info and
1306
                           info["meta_dev"] == meta.dev_path)
1307
      retval = retval and ("meta_index" in info and
1308
                           info["meta_index"] == 0)
1309
    else:
1310
      retval = retval and ("meta_dev" not in info and
1311
                           "meta_index" not in info)
1312
    return retval
1313

    
1314
  def _MatchesNet(self, info):
1315
    """Test if our network config matches with an existing device.
1316

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

1322
    """
1323
    if (((self._lhost is None and not ("local_addr" in info)) and
1324
         (self._rhost is None and not ("remote_addr" in info)))):
1325
      return True
1326

    
1327
    if self._lhost is None:
1328
      return False
1329

    
1330
    if not ("local_addr" in info and
1331
            "remote_addr" in info):
1332
      return False
1333

    
1334
    retval = (info["local_addr"] == (self._lhost, self._lport))
1335
    retval = (retval and
1336
              info["remote_addr"] == (self._rhost, self._rport))
1337
    return retval
1338

    
1339
  @classmethod
1340
  def _AssembleLocal(cls, minor, backend, meta, size):
1341
    """Configure the local part of a DRBD device.
1342

1343
    """
1344
    args = ["drbdsetup", cls._DevPath(minor), "disk",
1345
            backend, meta, "0",
1346
            "-e", "detach",
1347
            "--create-device"]
1348
    if size:
1349
      args.extend(["-d", "%sm" % size])
1350
    if not constants.DRBD_BARRIERS: # disable barriers, if configured so
1351
      version = cls._GetVersion(cls._GetProcData())
1352
      # various DRBD versions support different disk barrier options;
1353
      # what we aim here is to revert back to the 'drain' method of
1354
      # disk flushes and to disable metadata barriers, in effect going
1355
      # back to pre-8.0.7 behaviour
1356
      vmaj = version["k_major"]
1357
      vmin = version["k_minor"]
1358
      vrel = version["k_point"]
1359
      assert vmaj == 8
1360
      if vmin == 0: # 8.0.x
1361
        if vrel >= 12:
1362
          args.extend(["-i", "-m"])
1363
      elif vmin == 2: # 8.2.x
1364
        if vrel >= 7:
1365
          args.extend(["-i", "-m"])
1366
      elif vmaj >= 3: # 8.3.x or newer
1367
        args.extend(["-i", "-a", "m"])
1368
    result = utils.RunCmd(args)
1369
    if result.failed:
1370
      _ThrowError("drbd%d: can't attach local disk: %s", minor, result.output)
1371

    
1372
  @classmethod
1373
  def _AssembleNet(cls, minor, net_info, protocol,
1374
                   dual_pri=False, hmac=None, secret=None):
1375
    """Configure the network part of the device.
1376

1377
    """
1378
    lhost, lport, rhost, rport = net_info
1379
    if None in net_info:
1380
      # we don't want network connection and actually want to make
1381
      # sure its shutdown
1382
      cls._ShutdownNet(minor)
1383
      return
1384

    
1385
    # Workaround for a race condition. When DRBD is doing its dance to
1386
    # establish a connection with its peer, it also sends the
1387
    # synchronization speed over the wire. In some cases setting the
1388
    # sync speed only after setting up both sides can race with DRBD
1389
    # connecting, hence we set it here before telling DRBD anything
1390
    # about its peer.
1391
    cls._SetMinorSyncSpeed(minor, constants.SYNC_SPEED)
1392

    
1393
    if netutils.IP6Address.IsValid(lhost):
1394
      if not netutils.IP6Address.IsValid(rhost):
1395
        _ThrowError("drbd%d: can't connect ip %s to ip %s" %
1396
                    (minor, lhost, rhost))
1397
      family = "ipv6"
1398
    elif netutils.IP4Address.IsValid(lhost):
1399
      if not netutils.IP4Address.IsValid(rhost):
1400
        _ThrowError("drbd%d: can't connect ip %s to ip %s" %
1401
                    (minor, lhost, rhost))
1402
      family = "ipv4"
1403
    else:
1404
      _ThrowError("drbd%d: Invalid ip %s" % (minor, lhost))
1405

    
1406
    args = ["drbdsetup", cls._DevPath(minor), "net",
1407
            "%s:%s:%s" % (family, lhost, lport),
1408
            "%s:%s:%s" % (family, rhost, rport), protocol,
1409
            "-A", "discard-zero-changes",
1410
            "-B", "consensus",
1411
            "--create-device",
1412
            ]
1413
    if dual_pri:
1414
      args.append("-m")
1415
    if hmac and secret:
1416
      args.extend(["-a", hmac, "-x", secret])
1417
    result = utils.RunCmd(args)
1418
    if result.failed:
1419
      _ThrowError("drbd%d: can't setup network: %s - %s",
1420
                  minor, result.fail_reason, result.output)
1421

    
1422
    def _CheckNetworkConfig():
1423
      info = cls._GetDevInfo(cls._GetShowData(minor))
1424
      if not "local_addr" in info or not "remote_addr" in info:
1425
        raise utils.RetryAgain()
1426

    
1427
      if (info["local_addr"] != (lhost, lport) or
1428
          info["remote_addr"] != (rhost, rport)):
1429
        raise utils.RetryAgain()
1430

    
1431
    try:
1432
      utils.Retry(_CheckNetworkConfig, 1.0, 10.0)
1433
    except utils.RetryTimeout:
1434
      _ThrowError("drbd%d: timeout while configuring network", minor)
1435

    
1436
  def AddChildren(self, devices):
1437
    """Add a disk to the DRBD device.
1438

1439
    """
1440
    if self.minor is None:
1441
      _ThrowError("drbd%d: can't attach to dbrd8 during AddChildren",
1442
                  self._aminor)
1443
    if len(devices) != 2:
1444
      _ThrowError("drbd%d: need two devices for AddChildren", self.minor)
1445
    info = self._GetDevInfo(self._GetShowData(self.minor))
1446
    if "local_dev" in info:
1447
      _ThrowError("drbd%d: already attached to a local disk", self.minor)
1448
    backend, meta = devices
1449
    if backend.dev_path is None or meta.dev_path is None:
1450
      _ThrowError("drbd%d: children not ready during AddChildren", self.minor)
1451
    backend.Open()
1452
    meta.Open()
1453
    self._CheckMetaSize(meta.dev_path)
1454
    self._InitMeta(self._FindUnusedMinor(), meta.dev_path)
1455

    
1456
    self._AssembleLocal(self.minor, backend.dev_path, meta.dev_path, self.size)
1457
    self._children = devices
1458

    
1459
  def RemoveChildren(self, devices):
1460
    """Detach the drbd device from local storage.
1461

1462
    """
1463
    if self.minor is None:
1464
      _ThrowError("drbd%d: can't attach to drbd8 during RemoveChildren",
1465
                  self._aminor)
1466
    # early return if we don't actually have backing storage
1467
    info = self._GetDevInfo(self._GetShowData(self.minor))
1468
    if "local_dev" not in info:
1469
      return
1470
    if len(self._children) != 2:
1471
      _ThrowError("drbd%d: we don't have two children: %s", self.minor,
1472
                  self._children)
1473
    if self._children.count(None) == 2: # we don't actually have children :)
1474
      logging.warning("drbd%d: requested detach while detached", self.minor)
1475
      return
1476
    if len(devices) != 2:
1477
      _ThrowError("drbd%d: we need two children in RemoveChildren", self.minor)
1478
    for child, dev in zip(self._children, devices):
1479
      if dev != child.dev_path:
1480
        _ThrowError("drbd%d: mismatch in local storage (%s != %s) in"
1481
                    " RemoveChildren", self.minor, dev, child.dev_path)
1482

    
1483
    self._ShutdownLocal(self.minor)
1484
    self._children = []
1485

    
1486
  @classmethod
1487
  def _SetMinorSyncSpeed(cls, minor, kbytes):
1488
    """Set the speed of the DRBD syncer.
1489

1490
    This is the low-level implementation.
1491

1492
    @type minor: int
1493
    @param minor: the drbd minor whose settings we change
1494
    @type kbytes: int
1495
    @param kbytes: the speed in kbytes/second
1496
    @rtype: boolean
1497
    @return: the success of the operation
1498

1499
    """
1500
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "syncer",
1501
                           "-r", "%d" % kbytes, "--create-device"])
1502
    if result.failed:
1503
      logging.error("Can't change syncer rate: %s - %s",
1504
                    result.fail_reason, result.output)
1505
    return not result.failed
1506

    
1507
  def SetSyncSpeed(self, kbytes):
1508
    """Set the speed of the DRBD syncer.
1509

1510
    @type kbytes: int
1511
    @param kbytes: the speed in kbytes/second
1512
    @rtype: boolean
1513
    @return: the success of the operation
1514

1515
    """
1516
    if self.minor is None:
1517
      logging.info("Not attached during SetSyncSpeed")
1518
      return False
1519
    children_result = super(DRBD8, self).SetSyncSpeed(kbytes)
1520
    return self._SetMinorSyncSpeed(self.minor, kbytes) and children_result
1521

    
1522
  def PauseResumeSync(self, pause):
1523
    """Pauses or resumes the sync of a DRBD device.
1524

1525
    @param pause: Wether to pause or resume
1526
    @return: the success of the operation
1527

1528
    """
1529
    if self.minor is None:
1530
      logging.info("Not attached during PauseSync")
1531
      return False
1532

    
1533
    children_result = super(DRBD8, self).PauseResumeSync(pause)
1534

    
1535
    if pause:
1536
      cmd = "pause-sync"
1537
    else:
1538
      cmd = "resume-sync"
1539

    
1540
    result = utils.RunCmd(["drbdsetup", self.dev_path, cmd])
1541
    if result.failed:
1542
      logging.error("Can't %s: %s - %s", cmd,
1543
                    result.fail_reason, result.output)
1544
    return not result.failed and children_result
1545

    
1546
  def GetProcStatus(self):
1547
    """Return device data from /proc.
1548

1549
    """
1550
    if self.minor is None:
1551
      _ThrowError("drbd%d: GetStats() called while not attached", self._aminor)
1552
    proc_info = self._MassageProcData(self._GetProcData())
1553
    if self.minor not in proc_info:
1554
      _ThrowError("drbd%d: can't find myself in /proc", self.minor)
1555
    return DRBD8Status(proc_info[self.minor])
1556

    
1557
  def GetSyncStatus(self):
1558
    """Returns the sync status of the device.
1559

1560

1561
    If sync_percent is None, it means all is ok
1562
    If estimated_time is None, it means we can't estimate
1563
    the time needed, otherwise it's the time left in seconds.
1564

1565

1566
    We set the is_degraded parameter to True on two conditions:
1567
    network not connected or local disk missing.
1568

1569
    We compute the ldisk parameter based on whether we have a local
1570
    disk or not.
1571

1572
    @rtype: objects.BlockDevStatus
1573

1574
    """
1575
    if self.minor is None and not self.Attach():
1576
      _ThrowError("drbd%d: can't Attach() in GetSyncStatus", self._aminor)
1577

    
1578
    stats = self.GetProcStatus()
1579
    is_degraded = not stats.is_connected or not stats.is_disk_uptodate
1580

    
1581
    if stats.is_disk_uptodate:
1582
      ldisk_status = constants.LDS_OKAY
1583
    elif stats.is_diskless:
1584
      ldisk_status = constants.LDS_FAULTY
1585
    else:
1586
      ldisk_status = constants.LDS_UNKNOWN
1587

    
1588
    return objects.BlockDevStatus(dev_path=self.dev_path,
1589
                                  major=self.major,
1590
                                  minor=self.minor,
1591
                                  sync_percent=stats.sync_percent,
1592
                                  estimated_time=stats.est_time,
1593
                                  is_degraded=is_degraded,
1594
                                  ldisk_status=ldisk_status)
1595

    
1596
  def Open(self, force=False):
1597
    """Make the local state primary.
1598

1599
    If the 'force' parameter is given, the '-o' option is passed to
1600
    drbdsetup. Since this is a potentially dangerous operation, the
1601
    force flag should be only given after creation, when it actually
1602
    is mandatory.
1603

1604
    """
1605
    if self.minor is None and not self.Attach():
1606
      logging.error("DRBD cannot attach to a device during open")
1607
      return False
1608
    cmd = ["drbdsetup", self.dev_path, "primary"]
1609
    if force:
1610
      cmd.append("-o")
1611
    result = utils.RunCmd(cmd)
1612
    if result.failed:
1613
      _ThrowError("drbd%d: can't make drbd device primary: %s", self.minor,
1614
                  result.output)
1615

    
1616
  def Close(self):
1617
    """Make the local state secondary.
1618

1619
    This will, of course, fail if the device is in use.
1620

1621
    """
1622
    if self.minor is None and not self.Attach():
1623
      _ThrowError("drbd%d: can't Attach() in Close()", self._aminor)
1624
    result = utils.RunCmd(["drbdsetup", self.dev_path, "secondary"])
1625
    if result.failed:
1626
      _ThrowError("drbd%d: can't switch drbd device to secondary: %s",
1627
                  self.minor, result.output)
1628

    
1629
  def DisconnectNet(self):
1630
    """Removes network configuration.
1631

1632
    This method shutdowns the network side of the device.
1633

1634
    The method will wait up to a hardcoded timeout for the device to
1635
    go into standalone after the 'disconnect' command before
1636
    re-configuring it, as sometimes it takes a while for the
1637
    disconnect to actually propagate and thus we might issue a 'net'
1638
    command while the device is still connected. If the device will
1639
    still be attached to the network and we time out, we raise an
1640
    exception.
1641

1642
    """
1643
    if self.minor is None:
1644
      _ThrowError("drbd%d: disk not attached in re-attach net", self._aminor)
1645

    
1646
    if None in (self._lhost, self._lport, self._rhost, self._rport):
1647
      _ThrowError("drbd%d: DRBD disk missing network info in"
1648
                  " DisconnectNet()", self.minor)
1649

    
1650
    class _DisconnectStatus:
1651
      def __init__(self, ever_disconnected):
1652
        self.ever_disconnected = ever_disconnected
1653

    
1654
    dstatus = _DisconnectStatus(_IgnoreError(self._ShutdownNet, self.minor))
1655

    
1656
    def _WaitForDisconnect():
1657
      if self.GetProcStatus().is_standalone:
1658
        return
1659

    
1660
      # retry the disconnect, it seems possible that due to a well-time
1661
      # disconnect on the peer, my disconnect command might be ignored and
1662
      # forgotten
1663
      dstatus.ever_disconnected = \
1664
        _IgnoreError(self._ShutdownNet, self.minor) or dstatus.ever_disconnected
1665

    
1666
      raise utils.RetryAgain()
1667

    
1668
    # Keep start time
1669
    start_time = time.time()
1670

    
1671
    try:
1672
      # Start delay at 100 milliseconds and grow up to 2 seconds
1673
      utils.Retry(_WaitForDisconnect, (0.1, 1.5, 2.0),
1674
                  self._NET_RECONFIG_TIMEOUT)
1675
    except utils.RetryTimeout:
1676
      if dstatus.ever_disconnected:
1677
        msg = ("drbd%d: device did not react to the"
1678
               " 'disconnect' command in a timely manner")
1679
      else:
1680
        msg = "drbd%d: can't shutdown network, even after multiple retries"
1681

    
1682
      _ThrowError(msg, self.minor)
1683

    
1684
    reconfig_time = time.time() - start_time
1685
    if reconfig_time > (self._NET_RECONFIG_TIMEOUT * 0.25):
1686
      logging.info("drbd%d: DisconnectNet: detach took %.3f seconds",
1687
                   self.minor, reconfig_time)
1688

    
1689
  def AttachNet(self, multimaster):
1690
    """Reconnects the network.
1691

1692
    This method connects the network side of the device with a
1693
    specified multi-master flag. The device needs to be 'Standalone'
1694
    but have valid network configuration data.
1695

1696
    Args:
1697
      - multimaster: init the network in dual-primary mode
1698

1699
    """
1700
    if self.minor is None:
1701
      _ThrowError("drbd%d: device not attached in AttachNet", self._aminor)
1702

    
1703
    if None in (self._lhost, self._lport, self._rhost, self._rport):
1704
      _ThrowError("drbd%d: missing network info in AttachNet()", self.minor)
1705

    
1706
    status = self.GetProcStatus()
1707

    
1708
    if not status.is_standalone:
1709
      _ThrowError("drbd%d: device is not standalone in AttachNet", self.minor)
1710

    
1711
    self._AssembleNet(self.minor,
1712
                      (self._lhost, self._lport, self._rhost, self._rport),
1713
                      constants.DRBD_NET_PROTOCOL, dual_pri=multimaster,
1714
                      hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1715

    
1716
  def Attach(self):
1717
    """Check if our minor is configured.
1718

1719
    This doesn't do any device configurations - it only checks if the
1720
    minor is in a state different from Unconfigured.
1721

1722
    Note that this function will not change the state of the system in
1723
    any way (except in case of side-effects caused by reading from
1724
    /proc).
1725

1726
    """
1727
    used_devs = self.GetUsedDevs()
1728
    if self._aminor in used_devs:
1729
      minor = self._aminor
1730
    else:
1731
      minor = None
1732

    
1733
    self._SetFromMinor(minor)
1734
    return minor is not None
1735

    
1736
  def Assemble(self):
1737
    """Assemble the drbd.
1738

1739
    Method:
1740
      - if we have a configured device, we try to ensure that it matches
1741
        our config
1742
      - if not, we create it from zero
1743

1744
    """
1745
    super(DRBD8, self).Assemble()
1746

    
1747
    self.Attach()
1748
    if self.minor is None:
1749
      # local device completely unconfigured
1750
      self._FastAssemble()
1751
    else:
1752
      # we have to recheck the local and network status and try to fix
1753
      # the device
1754
      self._SlowAssemble()
1755

    
1756
  def _SlowAssemble(self):
1757
    """Assembles the DRBD device from a (partially) configured device.
1758

1759
    In case of partially attached (local device matches but no network
1760
    setup), we perform the network attach. If successful, we re-test
1761
    the attach if can return success.
1762

1763
    """
1764
    # TODO: Rewrite to not use a for loop just because there is 'break'
1765
    # pylint: disable-msg=W0631
1766
    net_data = (self._lhost, self._lport, self._rhost, self._rport)
1767
    for minor in (self._aminor,):
1768
      info = self._GetDevInfo(self._GetShowData(minor))
1769
      match_l = self._MatchesLocal(info)
1770
      match_r = self._MatchesNet(info)
1771

    
1772
      if match_l and match_r:
1773
        # everything matches
1774
        break
1775

    
1776
      if match_l and not match_r and "local_addr" not in info:
1777
        # disk matches, but not attached to network, attach and recheck
1778
        self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
1779
                          hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1780
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1781
          break
1782
        else:
1783
          _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
1784
                      " show' disagrees", minor)
1785

    
1786
      if match_r and "local_dev" not in info:
1787
        # no local disk, but network attached and it matches
1788
        self._AssembleLocal(minor, self._children[0].dev_path,
1789
                            self._children[1].dev_path, self.size)
1790
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1791
          break
1792
        else:
1793
          _ThrowError("drbd%d: disk attach successful, but 'drbdsetup"
1794
                      " show' disagrees", minor)
1795

    
1796
      # this case must be considered only if we actually have local
1797
      # storage, i.e. not in diskless mode, because all diskless
1798
      # devices are equal from the point of view of local
1799
      # configuration
1800
      if (match_l and "local_dev" in info and
1801
          not match_r and "local_addr" in info):
1802
        # strange case - the device network part points to somewhere
1803
        # else, even though its local storage is ours; as we own the
1804
        # drbd space, we try to disconnect from the remote peer and
1805
        # reconnect to our correct one
1806
        try:
1807
          self._ShutdownNet(minor)
1808
        except errors.BlockDeviceError, err:
1809
          _ThrowError("drbd%d: device has correct local storage, wrong"
1810
                      " remote peer and is unable to disconnect in order"
1811
                      " to attach to the correct peer: %s", minor, str(err))
1812
        # note: _AssembleNet also handles the case when we don't want
1813
        # local storage (i.e. one or more of the _[lr](host|port) is
1814
        # None)
1815
        self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
1816
                          hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1817
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1818
          break
1819
        else:
1820
          _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
1821
                      " show' disagrees", minor)
1822

    
1823
    else:
1824
      minor = None
1825

    
1826
    self._SetFromMinor(minor)
1827
    if minor is None:
1828
      _ThrowError("drbd%d: cannot activate, unknown or unhandled reason",
1829
                  self._aminor)
1830

    
1831
  def _FastAssemble(self):
1832
    """Assemble the drbd device from zero.
1833

1834
    This is run when in Assemble we detect our minor is unused.
1835

1836
    """
1837
    minor = self._aminor
1838
    if self._children and self._children[0] and self._children[1]:
1839
      self._AssembleLocal(minor, self._children[0].dev_path,
1840
                          self._children[1].dev_path, self.size)
1841
    if self._lhost and self._lport and self._rhost and self._rport:
1842
      self._AssembleNet(minor,
1843
                        (self._lhost, self._lport, self._rhost, self._rport),
1844
                        constants.DRBD_NET_PROTOCOL,
1845
                        hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1846
    self._SetFromMinor(minor)
1847

    
1848
  @classmethod
1849
  def _ShutdownLocal(cls, minor):
1850
    """Detach from the local device.
1851

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

1855
    """
1856
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "detach"])
1857
    if result.failed:
1858
      _ThrowError("drbd%d: can't detach local disk: %s", minor, result.output)
1859

    
1860
  @classmethod
1861
  def _ShutdownNet(cls, minor):
1862
    """Disconnect from the remote peer.
1863

1864
    This fails if we don't have a local device.
1865

1866
    """
1867
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disconnect"])
1868
    if result.failed:
1869
      _ThrowError("drbd%d: can't shutdown network: %s", minor, result.output)
1870

    
1871
  @classmethod
1872
  def _ShutdownAll(cls, minor):
1873
    """Deactivate the device.
1874

1875
    This will, of course, fail if the device is in use.
1876

1877
    """
1878
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "down"])
1879
    if result.failed:
1880
      _ThrowError("drbd%d: can't shutdown drbd device: %s",
1881
                  minor, result.output)
1882

    
1883
  def Shutdown(self):
1884
    """Shutdown the DRBD device.
1885

1886
    """
1887
    if self.minor is None and not self.Attach():
1888
      logging.info("drbd%d: not attached during Shutdown()", self._aminor)
1889
      return
1890
    minor = self.minor
1891
    self.minor = None
1892
    self.dev_path = None
1893
    self._ShutdownAll(minor)
1894

    
1895
  def Remove(self):
1896
    """Stub remove for DRBD devices.
1897

1898
    """
1899
    self.Shutdown()
1900

    
1901
  @classmethod
1902
  def Create(cls, unique_id, children, size):
1903
    """Create a new DRBD8 device.
1904

1905
    Since DRBD devices are not created per se, just assembled, this
1906
    function only initializes the metadata.
1907

1908
    """
1909
    if len(children) != 2:
1910
      raise errors.ProgrammerError("Invalid setup for the drbd device")
1911
    # check that the minor is unused
1912
    aminor = unique_id[4]
1913
    proc_info = cls._MassageProcData(cls._GetProcData())
1914
    if aminor in proc_info:
1915
      status = DRBD8Status(proc_info[aminor])
1916
      in_use = status.is_in_use
1917
    else:
1918
      in_use = False
1919
    if in_use:
1920
      _ThrowError("drbd%d: minor is already in use at Create() time", aminor)
1921
    meta = children[1]
1922
    meta.Assemble()
1923
    if not meta.Attach():
1924
      _ThrowError("drbd%d: can't attach to meta device '%s'",
1925
                  aminor, meta)
1926
    cls._CheckMetaSize(meta.dev_path)
1927
    cls._InitMeta(aminor, meta.dev_path)
1928
    return cls(unique_id, children, size)
1929

    
1930
  def Grow(self, amount, dryrun):
1931
    """Resize the DRBD device and its backing storage.
1932

1933
    """
1934
    if self.minor is None:
1935
      _ThrowError("drbd%d: Grow called while not attached", self._aminor)
1936
    if len(self._children) != 2 or None in self._children:
1937
      _ThrowError("drbd%d: cannot grow diskless device", self.minor)
1938
    self._children[0].Grow(amount, dryrun)
1939
    if dryrun:
1940
      # DRBD does not support dry-run mode, so we'll return here
1941
      return
1942
    result = utils.RunCmd(["drbdsetup", self.dev_path, "resize", "-s",
1943
                           "%dm" % (self.size + amount)])
1944
    if result.failed:
1945
      _ThrowError("drbd%d: resize failed: %s", self.minor, result.output)
1946

    
1947

    
1948
class FileStorage(BlockDev):
1949
  """File device.
1950

1951
  This class represents the a file storage backend device.
1952

1953
  The unique_id for the file device is a (file_driver, file_path) tuple.
1954

1955
  """
1956
  def __init__(self, unique_id, children, size):
1957
    """Initalizes a file device backend.
1958

1959
    """
1960
    if children:
1961
      raise errors.BlockDeviceError("Invalid setup for file device")
1962
    super(FileStorage, self).__init__(unique_id, children, size)
1963
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1964
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1965
    self.driver = unique_id[0]
1966
    self.dev_path = unique_id[1]
1967
    self.Attach()
1968

    
1969
  def Assemble(self):
1970
    """Assemble the device.
1971

1972
    Checks whether the file device exists, raises BlockDeviceError otherwise.
1973

1974
    """
1975
    if not os.path.exists(self.dev_path):
1976
      _ThrowError("File device '%s' does not exist" % self.dev_path)
1977

    
1978
  def Shutdown(self):
1979
    """Shutdown the device.
1980

1981
    This is a no-op for the file type, as we don't deactivate
1982
    the file on shutdown.
1983

1984
    """
1985
    pass
1986

    
1987
  def Open(self, force=False):
1988
    """Make the device ready for I/O.
1989

1990
    This is a no-op for the file type.
1991

1992
    """
1993
    pass
1994

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

1998
    This is a no-op for the file type.
1999

2000
    """
2001
    pass
2002

    
2003
  def Remove(self):
2004
    """Remove the file backing the block device.
2005

2006
    @rtype: boolean
2007
    @return: True if the removal was successful
2008

2009
    """
2010
    try:
2011
      os.remove(self.dev_path)
2012
    except OSError, err:
2013
      if err.errno != errno.ENOENT:
2014
        _ThrowError("Can't remove file '%s': %s", self.dev_path, err)
2015

    
2016
  def Rename(self, new_id):
2017
    """Renames the file.
2018

2019
    """
2020
    # TODO: implement rename for file-based storage
2021
    _ThrowError("Rename is not supported for file-based storage")
2022

    
2023
  def Grow(self, amount, dryrun):
2024
    """Grow the file
2025

2026
    @param amount: the amount (in mebibytes) to grow with
2027

2028
    """
2029
    # Check that the file exists
2030
    self.Assemble()
2031
    current_size = self.GetActualSize()
2032
    new_size = current_size + amount * 1024 * 1024
2033
    assert new_size > current_size, "Cannot Grow with a negative amount"
2034
    # We can't really simulate the growth
2035
    if dryrun:
2036
      return
2037
    try:
2038
      f = open(self.dev_path, "a+")
2039
      f.truncate(new_size)
2040
      f.close()
2041
    except EnvironmentError, err:
2042
      _ThrowError("Error in file growth: %", str(err))
2043

    
2044
  def Attach(self):
2045
    """Attach to an existing file.
2046

2047
    Check if this file already exists.
2048

2049
    @rtype: boolean
2050
    @return: True if file exists
2051

2052
    """
2053
    self.attached = os.path.exists(self.dev_path)
2054
    return self.attached
2055

    
2056
  def GetActualSize(self):
2057
    """Return the actual disk size.
2058

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

2061
    """
2062
    assert self.attached, "BlockDevice not attached in GetActualSize()"
2063
    try:
2064
      st = os.stat(self.dev_path)
2065
      return st.st_size
2066
    except OSError, err:
2067
      _ThrowError("Can't stat %s: %s", self.dev_path, err)
2068

    
2069
  @classmethod
2070
  def Create(cls, unique_id, children, size):
2071
    """Create a new file.
2072

2073
    @param size: the size of file in MiB
2074

2075
    @rtype: L{bdev.FileStorage}
2076
    @return: an instance of FileStorage
2077

2078
    """
2079
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
2080
      raise ValueError("Invalid configuration data %s" % str(unique_id))
2081
    dev_path = unique_id[1]
2082
    try:
2083
      fd = os.open(dev_path, os.O_RDWR | os.O_CREAT | os.O_EXCL)
2084
      f = os.fdopen(fd, "w")
2085
      f.truncate(size * 1024 * 1024)
2086
      f.close()
2087
    except EnvironmentError, err:
2088
      if err.errno == errno.EEXIST:
2089
        _ThrowError("File already existing: %s", dev_path)
2090
      _ThrowError("Error in file creation: %", str(err))
2091

    
2092
    return FileStorage(unique_id, children, size)
2093

    
2094

    
2095
class PersistentBlockDevice(BlockDev):
2096
  """A block device with persistent node
2097

2098
  May be either directly attached, or exposed through DM (e.g. dm-multipath).
2099
  udev helpers are probably required to give persistent, human-friendly
2100
  names.
2101

2102
  For the time being, pathnames are required to lie under /dev.
2103

2104
  """
2105
  def __init__(self, unique_id, children, size):
2106
    """Attaches to a static block device.
2107

2108
    The unique_id is a path under /dev.
2109

2110
    """
2111
    super(PersistentBlockDevice, self).__init__(unique_id, children, size)
2112
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
2113
      raise ValueError("Invalid configuration data %s" % str(unique_id))
2114
    self.dev_path = unique_id[1]
2115
    if not os.path.realpath(self.dev_path).startswith("/dev/"):
2116
      raise ValueError("Full path '%s' lies outside /dev" %
2117
                              os.path.realpath(self.dev_path))
2118
    # TODO: this is just a safety guard checking that we only deal with devices
2119
    # we know how to handle. In the future this will be integrated with
2120
    # external storage backends and possible values will probably be collected
2121
    # from the cluster configuration.
2122
    if unique_id[0] != constants.BLOCKDEV_DRIVER_MANUAL:
2123
      raise ValueError("Got persistent block device of invalid type: %s" %
2124
                       unique_id[0])
2125

    
2126
    self.major = self.minor = None
2127
    self.Attach()
2128

    
2129
  @classmethod
2130
  def Create(cls, unique_id, children, size):
2131
    """Create a new device
2132

2133
    This is a noop, we only return a PersistentBlockDevice instance
2134

2135
    """
2136
    return PersistentBlockDevice(unique_id, children, 0)
2137

    
2138
  def Remove(self):
2139
    """Remove a device
2140

2141
    This is a noop
2142

2143
    """
2144
    pass
2145

    
2146
  def Rename(self, new_id):
2147
    """Rename this device.
2148

2149
    """
2150
    _ThrowError("Rename is not supported for PersistentBlockDev storage")
2151

    
2152
  def Attach(self):
2153
    """Attach to an existing block device.
2154

2155

2156
    """
2157
    self.attached = False
2158
    try:
2159
      st = os.stat(self.dev_path)
2160
    except OSError, err:
2161
      logging.error("Error stat()'ing %s: %s", self.dev_path, str(err))
2162
      return False
2163

    
2164
    if not stat.S_ISBLK(st.st_mode):
2165
      logging.error("%s is not a block device", self.dev_path)
2166
      return False
2167

    
2168
    self.major = os.major(st.st_rdev)
2169
    self.minor = os.minor(st.st_rdev)
2170
    self.attached = True
2171

    
2172
    return True
2173

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

2177
    """
2178
    pass
2179

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

2183
    """
2184
    pass
2185

    
2186
  def Open(self, force=False):
2187
    """Make the device ready for I/O.
2188

2189
    """
2190
    pass
2191

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

2195
    """
2196
    pass
2197

    
2198
  def Grow(self, amount, dryrun):
2199
    """Grow the logical volume.
2200

2201
    """
2202
    _ThrowError("Grow is not supported for PersistentBlockDev storage")
2203

    
2204

    
2205
DEV_MAP = {
2206
  constants.LD_LV: LogicalVolume,
2207
  constants.LD_DRBD8: DRBD8,
2208
  constants.LD_BLOCKDEV: PersistentBlockDevice,
2209
  }
2210

    
2211
if constants.ENABLE_FILE_STORAGE or constants.ENABLE_SHARED_FILE_STORAGE:
2212
  DEV_MAP[constants.LD_FILE] = FileStorage
2213

    
2214

    
2215
def FindDevice(dev_type, unique_id, children, size):
2216
  """Search for an existing, assembled device.
2217

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

2221
  """
2222
  if dev_type not in DEV_MAP:
2223
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
2224
  device = DEV_MAP[dev_type](unique_id, children, size)
2225
  if not device.attached:
2226
    return None
2227
  return device
2228

    
2229

    
2230
def Assemble(dev_type, unique_id, children, size):
2231
  """Try to attach or assemble an existing device.
2232

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

2236
  """
2237
  if dev_type not in DEV_MAP:
2238
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
2239
  device = DEV_MAP[dev_type](unique_id, children, size)
2240
  device.Assemble()
2241
  return device
2242

    
2243

    
2244
def Create(dev_type, unique_id, children, size):
2245
  """Create a device.
2246

2247
  """
2248
  if dev_type not in DEV_MAP:
2249
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
2250
  device = DEV_MAP[dev_type].Create(unique_id, children, size)
2251
  return device