Statistics
| Branch: | Tag: | Revision:

root / lib / bdev.py @ 16629d10

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

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

328
    Only supported for some device types.
329

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

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

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

343
    """
344
    raise NotImplementedError
345

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

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

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

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

    
368

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
468
      data.append(splitted_fields)
469

    
470
    return data
471

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

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

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

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

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

    
500
    return data
501

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

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

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

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

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

    
531
    return data
532

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

659
    """
660
    pass
661

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

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

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

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

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

681
    @rtype: objects.BlockDevStatus
682

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

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

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

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

702
    """
703
    pass
704

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

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

710
    """
711
    pass
712

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

716
    @returns: tuple (vg, lv)
717

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

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

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

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

    
739
    return (self._vg_name, snap_name)
740

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

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

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

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

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

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

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

    
784

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

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

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

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

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

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

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

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

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

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

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

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

    
889

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
983
    return retval
984

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

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

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

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

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

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

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

    
1027
    return used_devs
1028

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

1032
    This sets our minor variable and our dev_path.
1033

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

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

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

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

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

1074
    This is not supported for drbd devices.
1075

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

    
1079

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

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

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

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

    
1096
  # timeout constants
1097
  _NET_RECONFIG_TIMEOUT = 60
1098

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1230
    cls._PARSE_SHOW = bnf
1231

    
1232
    return bnf
1233

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1491
    This is the low-level implementation.
1492

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1561

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

1566

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

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

1573
    @rtype: objects.BlockDevStatus
1574

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1667
      raise utils.RetryAgain()
1668

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

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

    
1683
      _ThrowError(msg, self.minor)
1684

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

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

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

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

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

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

    
1707
    status = self.GetProcStatus()
1708

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1824
    else:
1825
      minor = None
1826

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1899
    """
1900
    self.Shutdown()
1901

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

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

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

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

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

    
1948

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

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

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

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

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

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

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

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

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

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

1985
    """
1986
    pass
1987

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

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

1993
    """
1994
    pass
1995

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

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

2001
    """
2002
    pass
2003

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

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

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

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

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

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

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

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

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

2048
    Check if this file already exists.
2049

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

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

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

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

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

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

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

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

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

    
2093
    return FileStorage(unique_id, children, size)
2094

    
2095

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

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

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

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

2109
    The unique_id is a path under /dev.
2110

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

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

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

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

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

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

2142
    This is a noop
2143

2144
    """
2145
    pass
2146

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

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

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

2156

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

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

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

    
2173
    return True
2174

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

2178
    """
2179
    pass
2180

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

2184
    """
2185
    pass
2186

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

2190
    """
2191
    pass
2192

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

2196
    """
2197
    pass
2198

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

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

    
2205

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

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

    
2215

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

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

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

    
2230

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

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

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

    
2244

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

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