Statistics
| Branch: | Tag: | Revision:

root / lib / bdev.py @ 0304f0ec

History | View | Annotate | Download (71.7 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 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 at least 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
                       # Due to a bug in drbd in the kernel, introduced in
795
                       # commit 4b0715f096 (still unfixed as of 2011-08-22)
796
                       "(?:\s|M)"
797
                       "finish: ([0-9]+):([0-9]+):([0-9]+)\s.*$")
798

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

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

    
836
  RO_PRIMARY = "Primary"
837
  RO_SECONDARY = "Secondary"
838
  RO_UNKNOWN = "Unknown"
839

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

    
855
    # end reading of data from the LINE_RE or UNCONF_RE
856

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

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

    
870
    self.is_in_resync = self.cstatus in self.CSET_SYNC
871
    self.is_in_use = self.cstatus != self.CS_UNCONFIGURED
872

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

    
891

    
892
class BaseDRBD(BlockDev): # pylint: disable=W0223
893
  """Base DRBD class.
894

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

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

    
904
  _DRBD_MAJOR = 147
905
  _ST_UNCONFIGURED = "Unconfigured"
906
  _ST_WFCONNECTION = "WFConnection"
907
  _ST_CONNECTED = "Connected"
908

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

    
912
  @staticmethod
913
  def _GetProcData(filename=_STATUS_FILE):
914
    """Return data from /proc/drbd.
915

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

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

933
    @return: a dictionary of minor: joined lines from /proc/drbd
934
        for that minor
935

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

    
956
  @classmethod
957
  def _GetVersion(cls, proc_data):
958
    """Return the DRBD version.
959

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

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

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

    
985
    return retval
986

    
987
  @staticmethod
988
  def GetUsermodeHelper(filename=_USERMODE_HELPER_FILE):
989
    """Returns DRBD usermode_helper currently set.
990

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

    
1004
  @staticmethod
1005
  def _DevPath(minor):
1006
    """Return the path to a drbd device for a given minor.
1007

1008
    """
1009
    return "/dev/drbd%d" % minor
1010

    
1011
  @classmethod
1012
  def GetUsedDevs(cls):
1013
    """Compute the list of used DRBD devices.
1014

1015
    """
1016
    data = cls._GetProcData()
1017

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

    
1029
    return used_devs
1030

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

1034
    This sets our minor variable and our dev_path.
1035

1036
    """
1037
    if minor is None:
1038
      self.minor = self.dev_path = None
1039
      self.attached = False
1040
    else:
1041
      self.minor = minor
1042
      self.dev_path = self._DevPath(minor)
1043
      self.attached = True
1044

    
1045
  @staticmethod
1046
  def _CheckMetaSize(meta_device):
1047
    """Check if the given meta device looks like a valid one.
1048

1049
    This currently only check the size, which must be around
1050
    128MiB.
1051

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

    
1073
  def Rename(self, new_id):
1074
    """Rename a device.
1075

1076
    This is not supported for drbd devices.
1077

1078
    """
1079
    raise errors.ProgrammerError("Can't rename a drbd device")
1080

    
1081

    
1082
class DRBD8(BaseDRBD):
1083
  """DRBD v8.x block device.
1084

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

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

1094
  """
1095
  _MAX_MINORS = 255
1096
  _PARSE_SHOW = None
1097

    
1098
  # timeout constants
1099
  _NET_RECONFIG_TIMEOUT = 60
1100

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

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

    
1129
  @classmethod
1130
  def _InitMeta(cls, minor, dev_path):
1131
    """Initialize a meta device.
1132

1133
    This will not work if the given minor is in use.
1134

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

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

    
1152
  @classmethod
1153
  def _FindUnusedMinor(cls):
1154
    """Find an unused DRBD device.
1155

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

1159
    """
1160
    data = cls._GetProcData()
1161

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

    
1178
  @classmethod
1179
  def _GetShowParser(cls):
1180
    """Return a parser for `drbd show` output.
1181

1182
    This will either create or return an already-create parser for the
1183
    output of the command `drbd show`.
1184

1185
    """
1186
    if cls._PARSE_SHOW is not None:
1187
      return cls._PARSE_SHOW
1188

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

    
1199
    comment = pyp.Literal("#") + pyp.Optional(pyp.restOfLine)
1200
    defa = pyp.Literal("_is_default").suppress()
1201
    dbl_quote = pyp.Literal('"').suppress()
1202

    
1203
    keyword = pyp.Word(pyp.alphanums + '-')
1204

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

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

    
1225
    # an entire section
1226
    section_name = pyp.Word(pyp.alphas + "_")
1227
    section = section_name + lbrace + pyp.ZeroOrMore(pyp.Group(stmt)) + rbrace
1228

    
1229
    bnf = pyp.ZeroOrMore(pyp.Group(section ^ stmt))
1230
    bnf.ignore(comment)
1231

    
1232
    cls._PARSE_SHOW = bnf
1233

    
1234
    return bnf
1235

    
1236
  @classmethod
1237
  def _GetShowData(cls, minor):
1238
    """Return the `drbdsetup show` data for a minor.
1239

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

    
1248
  @classmethod
1249
  def _GetDevInfo(cls, out):
1250
    """Parse details about a given DRBD minor.
1251

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

1257
    """
1258
    data = {}
1259
    if not out:
1260
      return data
1261

    
1262
    bnf = cls._GetShowParser()
1263
    # run pyparse
1264

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

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

    
1288
  def _MatchesLocal(self, info):
1289
    """Test if our local config matches with an existing device.
1290

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

1296
    """
1297
    if self._children:
1298
      backend, meta = self._children
1299
    else:
1300
      backend = meta = None
1301

    
1302
    if backend is not None:
1303
      retval = ("local_dev" in info and info["local_dev"] == backend.dev_path)
1304
    else:
1305
      retval = ("local_dev" not in info)
1306

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

    
1317
  def _MatchesNet(self, info):
1318
    """Test if our network config matches with an existing device.
1319

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

1325
    """
1326
    if (((self._lhost is None and not ("local_addr" in info)) and
1327
         (self._rhost is None and not ("remote_addr" in info)))):
1328
      return True
1329

    
1330
    if self._lhost is None:
1331
      return False
1332

    
1333
    if not ("local_addr" in info and
1334
            "remote_addr" in info):
1335
      return False
1336

    
1337
    retval = (info["local_addr"] == (self._lhost, self._lport))
1338
    retval = (retval and
1339
              info["remote_addr"] == (self._rhost, self._rport))
1340
    return retval
1341

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

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

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

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

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

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

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

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

    
1430
      if (info["local_addr"] != (lhost, lport) or
1431
          info["remote_addr"] != (rhost, rport)):
1432
        raise utils.RetryAgain()
1433

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

    
1439
  def AddChildren(self, devices):
1440
    """Add a disk to the DRBD device.
1441

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

    
1459
    self._AssembleLocal(self.minor, backend.dev_path, meta.dev_path, self.size)
1460
    self._children = devices
1461

    
1462
  def RemoveChildren(self, devices):
1463
    """Detach the drbd device from local storage.
1464

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

    
1486
    self._ShutdownLocal(self.minor)
1487
    self._children = []
1488

    
1489
  @classmethod
1490
  def _SetMinorSyncSpeed(cls, minor, kbytes):
1491
    """Set the speed of the DRBD syncer.
1492

1493
    This is the low-level implementation.
1494

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

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

    
1510
  def SetSyncSpeed(self, kbytes):
1511
    """Set the speed of the DRBD syncer.
1512

1513
    @type kbytes: int
1514
    @param kbytes: the speed in kbytes/second
1515
    @rtype: boolean
1516
    @return: the success of the operation
1517

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

    
1525
  def PauseResumeSync(self, pause):
1526
    """Pauses or resumes the sync of a DRBD device.
1527

1528
    @param pause: Wether to pause or resume
1529
    @return: the success of the operation
1530

1531
    """
1532
    if self.minor is None:
1533
      logging.info("Not attached during PauseSync")
1534
      return False
1535

    
1536
    children_result = super(DRBD8, self).PauseResumeSync(pause)
1537

    
1538
    if pause:
1539
      cmd = "pause-sync"
1540
    else:
1541
      cmd = "resume-sync"
1542

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

    
1549
  def GetProcStatus(self):
1550
    """Return device data from /proc.
1551

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

    
1560
  def GetSyncStatus(self):
1561
    """Returns the sync status of the device.
1562

1563

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

1568

1569
    We set the is_degraded parameter to True on two conditions:
1570
    network not connected or local disk missing.
1571

1572
    We compute the ldisk parameter based on whether we have a local
1573
    disk or not.
1574

1575
    @rtype: objects.BlockDevStatus
1576

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

    
1581
    stats = self.GetProcStatus()
1582
    is_degraded = not stats.is_connected or not stats.is_disk_uptodate
1583

    
1584
    if stats.is_disk_uptodate:
1585
      ldisk_status = constants.LDS_OKAY
1586
    elif stats.is_diskless:
1587
      ldisk_status = constants.LDS_FAULTY
1588
    else:
1589
      ldisk_status = constants.LDS_UNKNOWN
1590

    
1591
    return objects.BlockDevStatus(dev_path=self.dev_path,
1592
                                  major=self.major,
1593
                                  minor=self.minor,
1594
                                  sync_percent=stats.sync_percent,
1595
                                  estimated_time=stats.est_time,
1596
                                  is_degraded=is_degraded,
1597
                                  ldisk_status=ldisk_status)
1598

    
1599
  def Open(self, force=False):
1600
    """Make the local state primary.
1601

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

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

    
1619
  def Close(self):
1620
    """Make the local state secondary.
1621

1622
    This will, of course, fail if the device is in use.
1623

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

    
1632
  def DisconnectNet(self):
1633
    """Removes network configuration.
1634

1635
    This method shutdowns the network side of the device.
1636

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

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

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

    
1653
    class _DisconnectStatus:
1654
      def __init__(self, ever_disconnected):
1655
        self.ever_disconnected = ever_disconnected
1656

    
1657
    dstatus = _DisconnectStatus(_IgnoreError(self._ShutdownNet, self.minor))
1658

    
1659
    def _WaitForDisconnect():
1660
      if self.GetProcStatus().is_standalone:
1661
        return
1662

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

    
1669
      raise utils.RetryAgain()
1670

    
1671
    # Keep start time
1672
    start_time = time.time()
1673

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

    
1685
      _ThrowError(msg, self.minor)
1686

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

    
1692
  def AttachNet(self, multimaster):
1693
    """Reconnects the network.
1694

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

1699
    Args:
1700
      - multimaster: init the network in dual-primary mode
1701

1702
    """
1703
    if self.minor is None:
1704
      _ThrowError("drbd%d: device not attached in AttachNet", self._aminor)
1705

    
1706
    if None in (self._lhost, self._lport, self._rhost, self._rport):
1707
      _ThrowError("drbd%d: missing network info in AttachNet()", self.minor)
1708

    
1709
    status = self.GetProcStatus()
1710

    
1711
    if not status.is_standalone:
1712
      _ThrowError("drbd%d: device is not standalone in AttachNet", self.minor)
1713

    
1714
    self._AssembleNet(self.minor,
1715
                      (self._lhost, self._lport, self._rhost, self._rport),
1716
                      constants.DRBD_NET_PROTOCOL, dual_pri=multimaster,
1717
                      hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1718

    
1719
  def Attach(self):
1720
    """Check if our minor is configured.
1721

1722
    This doesn't do any device configurations - it only checks if the
1723
    minor is in a state different from Unconfigured.
1724

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

1729
    """
1730
    used_devs = self.GetUsedDevs()
1731
    if self._aminor in used_devs:
1732
      minor = self._aminor
1733
    else:
1734
      minor = None
1735

    
1736
    self._SetFromMinor(minor)
1737
    return minor is not None
1738

    
1739
  def Assemble(self):
1740
    """Assemble the drbd.
1741

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

1747
    """
1748
    super(DRBD8, self).Assemble()
1749

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

    
1759
  def _SlowAssemble(self):
1760
    """Assembles the DRBD device from a (partially) configured device.
1761

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

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

    
1775
      if match_l and match_r:
1776
        # everything matches
1777
        break
1778

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

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

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

    
1826
    else:
1827
      minor = None
1828

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

    
1834
  def _FastAssemble(self):
1835
    """Assemble the drbd device from zero.
1836

1837
    This is run when in Assemble we detect our minor is unused.
1838

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

    
1851
  @classmethod
1852
  def _ShutdownLocal(cls, minor):
1853
    """Detach from the local device.
1854

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

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

    
1863
  @classmethod
1864
  def _ShutdownNet(cls, minor):
1865
    """Disconnect from the remote peer.
1866

1867
    This fails if we don't have a local device.
1868

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

    
1874
  @classmethod
1875
  def _ShutdownAll(cls, minor):
1876
    """Deactivate the device.
1877

1878
    This will, of course, fail if the device is in use.
1879

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

    
1886
  def Shutdown(self):
1887
    """Shutdown the DRBD device.
1888

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

    
1898
  def Remove(self):
1899
    """Stub remove for DRBD devices.
1900

1901
    """
1902
    self.Shutdown()
1903

    
1904
  @classmethod
1905
  def Create(cls, unique_id, children, size):
1906
    """Create a new DRBD8 device.
1907

1908
    Since DRBD devices are not created per se, just assembled, this
1909
    function only initializes the metadata.
1910

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

    
1933
  def Grow(self, amount, dryrun):
1934
    """Resize the DRBD device and its backing storage.
1935

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

    
1950

    
1951
class FileStorage(BlockDev):
1952
  """File device.
1953

1954
  This class represents the a file storage backend device.
1955

1956
  The unique_id for the file device is a (file_driver, file_path) tuple.
1957

1958
  """
1959
  def __init__(self, unique_id, children, size):
1960
    """Initalizes a file device backend.
1961

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

    
1972
  def Assemble(self):
1973
    """Assemble the device.
1974

1975
    Checks whether the file device exists, raises BlockDeviceError otherwise.
1976

1977
    """
1978
    if not os.path.exists(self.dev_path):
1979
      _ThrowError("File device '%s' does not exist" % self.dev_path)
1980

    
1981
  def Shutdown(self):
1982
    """Shutdown the device.
1983

1984
    This is a no-op for the file type, as we don't deactivate
1985
    the file on shutdown.
1986

1987
    """
1988
    pass
1989

    
1990
  def Open(self, force=False):
1991
    """Make the device ready for I/O.
1992

1993
    This is a no-op for the file type.
1994

1995
    """
1996
    pass
1997

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

2001
    This is a no-op for the file type.
2002

2003
    """
2004
    pass
2005

    
2006
  def Remove(self):
2007
    """Remove the file backing the block device.
2008

2009
    @rtype: boolean
2010
    @return: True if the removal was successful
2011

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

    
2019
  def Rename(self, new_id):
2020
    """Renames the file.
2021

2022
    """
2023
    # TODO: implement rename for file-based storage
2024
    _ThrowError("Rename is not supported for file-based storage")
2025

    
2026
  def Grow(self, amount, dryrun):
2027
    """Grow the file
2028

2029
    @param amount: the amount (in mebibytes) to grow with
2030

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

    
2047
  def Attach(self):
2048
    """Attach to an existing file.
2049

2050
    Check if this file already exists.
2051

2052
    @rtype: boolean
2053
    @return: True if file exists
2054

2055
    """
2056
    self.attached = os.path.exists(self.dev_path)
2057
    return self.attached
2058

    
2059
  def GetActualSize(self):
2060
    """Return the actual disk size.
2061

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

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

    
2072
  @classmethod
2073
  def Create(cls, unique_id, children, size):
2074
    """Create a new file.
2075

2076
    @param size: the size of file in MiB
2077

2078
    @rtype: L{bdev.FileStorage}
2079
    @return: an instance of FileStorage
2080

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

    
2095
    return FileStorage(unique_id, children, size)
2096

    
2097

    
2098
class PersistentBlockDevice(BlockDev):
2099
  """A block device with persistent node
2100

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

2105
  For the time being, pathnames are required to lie under /dev.
2106

2107
  """
2108
  def __init__(self, unique_id, children, size):
2109
    """Attaches to a static block device.
2110

2111
    The unique_id is a path under /dev.
2112

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

    
2129
    self.major = self.minor = None
2130
    self.Attach()
2131

    
2132
  @classmethod
2133
  def Create(cls, unique_id, children, size):
2134
    """Create a new device
2135

2136
    This is a noop, we only return a PersistentBlockDevice instance
2137

2138
    """
2139
    return PersistentBlockDevice(unique_id, children, 0)
2140

    
2141
  def Remove(self):
2142
    """Remove a device
2143

2144
    This is a noop
2145

2146
    """
2147
    pass
2148

    
2149
  def Rename(self, new_id):
2150
    """Rename this device.
2151

2152
    """
2153
    _ThrowError("Rename is not supported for PersistentBlockDev storage")
2154

    
2155
  def Attach(self):
2156
    """Attach to an existing block device.
2157

2158

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

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

    
2171
    self.major = os.major(st.st_rdev)
2172
    self.minor = os.minor(st.st_rdev)
2173
    self.attached = True
2174

    
2175
    return True
2176

    
2177
  def Assemble(self):
2178
    """Assemble the device.
2179

2180
    """
2181
    pass
2182

    
2183
  def Shutdown(self):
2184
    """Shutdown the device.
2185

2186
    """
2187
    pass
2188

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

2192
    """
2193
    pass
2194

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

2198
    """
2199
    pass
2200

    
2201
  def Grow(self, amount, dryrun):
2202
    """Grow the logical volume.
2203

2204
    """
2205
    _ThrowError("Grow is not supported for PersistentBlockDev storage")
2206

    
2207

    
2208
DEV_MAP = {
2209
  constants.LD_LV: LogicalVolume,
2210
  constants.LD_DRBD8: DRBD8,
2211
  constants.LD_BLOCKDEV: PersistentBlockDevice,
2212
  }
2213

    
2214
if constants.ENABLE_FILE_STORAGE or constants.ENABLE_SHARED_FILE_STORAGE:
2215
  DEV_MAP[constants.LD_FILE] = FileStorage
2216

    
2217

    
2218
def FindDevice(dev_type, unique_id, children, size):
2219
  """Search for an existing, assembled device.
2220

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

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

    
2232

    
2233
def Assemble(dev_type, unique_id, children, size):
2234
  """Try to attach or assemble an existing device.
2235

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

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

    
2246

    
2247
def Create(dev_type, unique_id, children, size):
2248
  """Create a device.
2249

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