Statistics
| Branch: | Tag: | Revision:

root / lib / bdev.py @ b6135bbc

History | View | Annotate | Download (70.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):
335
    """Grow the block device.
336

337
    @param amount: the amount (in mebibytes) to grow with
338

339
    """
340
    raise NotImplementedError
341

    
342
  def GetActualSize(self):
343
    """Return the actual disk size.
344

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

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

    
359
  def __repr__(self):
360
    return ("<%s: unique_id: %s, children: %s, %s:%s, %s>" %
361
            (self.__class__, self.unique_id, self._children,
362
             self.major, self.minor, self.dev_path))
363

    
364

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

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

    
373
  def __init__(self, unique_id, children, size):
374
    """Attaches to a LV device.
375

376
    The unique_id is a tuple (vg_name, lv_name)
377

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

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

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

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

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

    
435
  @staticmethod
436
  def _GetVolumeInfo(lvm_cmd, fields):
437
    """Returns LVM Volumen infos using lvm_cmd
438

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

443
    """
444
    if not fields:
445
      raise errors.ProgrammerError("No fields specified")
446

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

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

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

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

    
464
      data.append(splitted_fields)
465

    
466
    return data
467

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

472
    @param vg_names: list of volume group names, if empty all will be returned
473
    @param filter_allocatable: whether to skip over unallocatable PVs
474

475
    @rtype: list
476
    @return: list of tuples (free_space, name) with free_space in mebibytes
477

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

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

    
496
    return data
497

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

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

505
    @rtype: list
506
    @return: list of tuples (free_space, total_size, name) with free_space in
507
             MiB
508

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

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

    
527
    return data
528

    
529
  @classmethod
530
  def _ValidateName(cls, name):
531
    """Validates that a given name is valid as VG or LV name.
532

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

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

    
543
  def Remove(self):
544
    """Remove this logical volume.
545

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

    
555
  def Rename(self, new_id):
556
    """Rename this logical volume.
557

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

    
572
  def Attach(self):
573
    """Attach to an existing LV.
574

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

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

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

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

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

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

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

    
637
  def Assemble(self):
638
    """Assemble the device.
639

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

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

    
649
  def Shutdown(self):
650
    """Shutdown the device.
651

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

655
    """
656
    pass
657

    
658
  def GetSyncStatus(self):
659
    """Returns the sync status of the device.
660

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

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

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

675
    The status was already read in Attach, so we just return it.
676

677
    @rtype: objects.BlockDevStatus
678

679
    """
680
    if self._degraded:
681
      ldisk_status = constants.LDS_FAULTY
682
    else:
683
      ldisk_status = constants.LDS_OKAY
684

    
685
    return objects.BlockDevStatus(dev_path=self.dev_path,
686
                                  major=self.major,
687
                                  minor=self.minor,
688
                                  sync_percent=None,
689
                                  estimated_time=None,
690
                                  is_degraded=self._degraded,
691
                                  ldisk_status=ldisk_status)
692

    
693
  def Open(self, force=False):
694
    """Make the device ready for I/O.
695

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

698
    """
699
    pass
700

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

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

706
    """
707
    pass
708

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

712
    @returns: tuple (vg, lv)
713

714
    """
715
    snap_name = self._lv_name + ".snap"
716

    
717
    # remove existing snapshot if found
718
    snap = LogicalVolume((self._vg_name, snap_name), None, size)
719
    _IgnoreError(snap.Remove)
720

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

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

    
735
    return (self._vg_name, snap_name)
736

    
737
  def SetInfo(self, text):
738
    """Update metadata with info text.
739

740
    """
741
    BlockDev.SetInfo(self, text)
742

    
743
    # Replace invalid characters
744
    text = re.sub('^[^A-Za-z0-9_+.]', '_', text)
745
    text = re.sub('[^-A-Za-z0-9_+.]', '_', text)
746

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

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

    
756
  def Grow(self, amount):
757
    """Grow the logical volume.
758

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

    
778

    
779
class DRBD8Status(object):
780
  """A DRBD status representation class.
781

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

784
  """
785
  UNCONF_RE = re.compile(r"\s*[0-9]+:\s*cs:Unconfigured$")
786
  LINE_RE = re.compile(r"\s*[0-9]+:\s*cs:(\S+)\s+(?:st|ro):([^/]+)/(\S+)"
787
                       "\s+ds:([^/]+)/(\S+)\s+.*$")
788
  SYNC_RE = re.compile(r"^.*\ssync'ed:\s*([0-9.]+)%.*"
789
                       "\sfinish: ([0-9]+):([0-9]+):([0-9]+)\s.*$")
790

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

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

    
828
  RO_PRIMARY = "Primary"
829
  RO_SECONDARY = "Secondary"
830
  RO_UNKNOWN = "Unknown"
831

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

    
847
    # end reading of data from the LINE_RE or UNCONF_RE
848

    
849
    self.is_standalone = self.cstatus == self.CS_STANDALONE
850
    self.is_wfconn = self.cstatus == self.CS_WFCONNECTION
851
    self.is_connected = self.cstatus == self.CS_CONNECTED
852
    self.is_primary = self.lrole == self.RO_PRIMARY
853
    self.is_secondary = self.lrole == self.RO_SECONDARY
854
    self.peer_primary = self.rrole == self.RO_PRIMARY
855
    self.peer_secondary = self.rrole == self.RO_SECONDARY
856
    self.both_primary = self.is_primary and self.peer_primary
857
    self.both_secondary = self.is_secondary and self.peer_secondary
858

    
859
    self.is_diskless = self.ldisk == self.DS_DISKLESS
860
    self.is_disk_uptodate = self.ldisk == self.DS_UPTODATE
861

    
862
    self.is_in_resync = self.cstatus in self.CSET_SYNC
863
    self.is_in_use = self.cstatus != self.CS_UNCONFIGURED
864

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

    
883

    
884
class BaseDRBD(BlockDev): # pylint: disable-msg=W0223
885
  """Base DRBD class.
886

887
  This class contains a few bits of common functionality between the
888
  0.7 and 8.x versions of DRBD.
889

890
  """
891
  _VERSION_RE = re.compile(r"^version: (\d+)\.(\d+)\.(\d+)(?:\.\d+)?"
892
                           r" \(api:(\d+)/proto:(\d+)(?:-(\d+))?\)")
893
  _VALID_LINE_RE = re.compile("^ *([0-9]+): cs:([^ ]+).*$")
894
  _UNUSED_LINE_RE = re.compile("^ *([0-9]+): cs:Unconfigured$")
895

    
896
  _DRBD_MAJOR = 147
897
  _ST_UNCONFIGURED = "Unconfigured"
898
  _ST_WFCONNECTION = "WFConnection"
899
  _ST_CONNECTED = "Connected"
900

    
901
  _STATUS_FILE = "/proc/drbd"
902
  _USERMODE_HELPER_FILE = "/sys/module/drbd/parameters/usermode_helper"
903

    
904
  @staticmethod
905
  def _GetProcData(filename=_STATUS_FILE):
906
    """Return data from /proc/drbd.
907

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

    
921
  @classmethod
922
  def _MassageProcData(cls, data):
923
    """Transform the output of _GetProdData into a nicer form.
924

925
    @return: a dictionary of minor: joined lines from /proc/drbd
926
        for that minor
927

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

    
948
  @classmethod
949
  def _GetVersion(cls, proc_data):
950
    """Return the DRBD version.
951

952
    This will return a dict with keys:
953
      - k_major
954
      - k_minor
955
      - k_point
956
      - api
957
      - proto
958
      - proto2 (only on drbd > 8.2.X)
959

960
    """
961
    first_line = proc_data[0].strip()
962
    version = cls._VERSION_RE.match(first_line)
963
    if not version:
964
      raise errors.BlockDeviceError("Can't parse DRBD version from '%s'" %
965
                                    first_line)
966

    
967
    values = version.groups()
968
    retval = {'k_major': int(values[0]),
969
              'k_minor': int(values[1]),
970
              'k_point': int(values[2]),
971
              'api': int(values[3]),
972
              'proto': int(values[4]),
973
             }
974
    if values[5] is not None:
975
      retval['proto2'] = values[5]
976

    
977
    return retval
978

    
979
  @staticmethod
980
  def GetUsermodeHelper(filename=_USERMODE_HELPER_FILE):
981
    """Returns DRBD usermode_helper currently set.
982

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

    
996
  @staticmethod
997
  def _DevPath(minor):
998
    """Return the path to a drbd device for a given minor.
999

1000
    """
1001
    return "/dev/drbd%d" % minor
1002

    
1003
  @classmethod
1004
  def GetUsedDevs(cls):
1005
    """Compute the list of used DRBD devices.
1006

1007
    """
1008
    data = cls._GetProcData()
1009

    
1010
    used_devs = {}
1011
    for line in data:
1012
      match = cls._VALID_LINE_RE.match(line)
1013
      if not match:
1014
        continue
1015
      minor = int(match.group(1))
1016
      state = match.group(2)
1017
      if state == cls._ST_UNCONFIGURED:
1018
        continue
1019
      used_devs[minor] = state, line
1020

    
1021
    return used_devs
1022

    
1023
  def _SetFromMinor(self, minor):
1024
    """Set our parameters based on the given minor.
1025

1026
    This sets our minor variable and our dev_path.
1027

1028
    """
1029
    if minor is None:
1030
      self.minor = self.dev_path = None
1031
      self.attached = False
1032
    else:
1033
      self.minor = minor
1034
      self.dev_path = self._DevPath(minor)
1035
      self.attached = True
1036

    
1037
  @staticmethod
1038
  def _CheckMetaSize(meta_device):
1039
    """Check if the given meta device looks like a valid one.
1040

1041
    This currently only check the size, which must be around
1042
    128MiB.
1043

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

    
1065
  def Rename(self, new_id):
1066
    """Rename a device.
1067

1068
    This is not supported for drbd devices.
1069

1070
    """
1071
    raise errors.ProgrammerError("Can't rename a drbd device")
1072

    
1073

    
1074
class DRBD8(BaseDRBD):
1075
  """DRBD v8.x block device.
1076

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

1081
  The unique_id for the drbd device is the (local_ip, local_port,
1082
  remote_ip, remote_port) tuple, and it must have two children: the
1083
  data device and the meta_device. The meta device is checked for
1084
  valid size and is zeroed on create.
1085

1086
  """
1087
  _MAX_MINORS = 255
1088
  _PARSE_SHOW = None
1089

    
1090
  # timeout constants
1091
  _NET_RECONFIG_TIMEOUT = 60
1092

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

    
1115
    if (self._lhost is not None and self._lhost == self._rhost and
1116
        self._lport == self._rport):
1117
      raise ValueError("Invalid configuration data, same local/remote %s" %
1118
                       (unique_id,))
1119
    self.Attach()
1120

    
1121
  @classmethod
1122
  def _InitMeta(cls, minor, dev_path):
1123
    """Initialize a meta device.
1124

1125
    This will not work if the given minor is in use.
1126

1127
    """
1128
    result = utils.RunCmd(["drbdmeta", "--force", cls._DevPath(minor),
1129
                           "v08", dev_path, "0", "create-md"])
1130
    if result.failed:
1131
      _ThrowError("Can't initialize meta device: %s", result.output)
1132

    
1133
  @classmethod
1134
  def _FindUnusedMinor(cls):
1135
    """Find an unused DRBD device.
1136

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

1140
    """
1141
    data = cls._GetProcData()
1142

    
1143
    highest = None
1144
    for line in data:
1145
      match = cls._UNUSED_LINE_RE.match(line)
1146
      if match:
1147
        return int(match.group(1))
1148
      match = cls._VALID_LINE_RE.match(line)
1149
      if match:
1150
        minor = int(match.group(1))
1151
        highest = max(highest, minor)
1152
    if highest is None: # there are no minors in use at all
1153
      return 0
1154
    if highest >= cls._MAX_MINORS:
1155
      logging.error("Error: no free drbd minors!")
1156
      raise errors.BlockDeviceError("Can't find a free DRBD minor")
1157
    return highest + 1
1158

    
1159
  @classmethod
1160
  def _GetShowParser(cls):
1161
    """Return a parser for `drbd show` output.
1162

1163
    This will either create or return an already-create parser for the
1164
    output of the command `drbd show`.
1165

1166
    """
1167
    if cls._PARSE_SHOW is not None:
1168
      return cls._PARSE_SHOW
1169

    
1170
    # pyparsing setup
1171
    lbrace = pyp.Literal("{").suppress()
1172
    rbrace = pyp.Literal("}").suppress()
1173
    lbracket = pyp.Literal("[").suppress()
1174
    rbracket = pyp.Literal("]").suppress()
1175
    semi = pyp.Literal(";").suppress()
1176
    colon = pyp.Literal(":").suppress()
1177
    # this also converts the value to an int
1178
    number = pyp.Word(pyp.nums).setParseAction(lambda s, l, t: int(t[0]))
1179

    
1180
    comment = pyp.Literal ("#") + pyp.Optional(pyp.restOfLine)
1181
    defa = pyp.Literal("_is_default").suppress()
1182
    dbl_quote = pyp.Literal('"').suppress()
1183

    
1184
    keyword = pyp.Word(pyp.alphanums + '-')
1185

    
1186
    # value types
1187
    value = pyp.Word(pyp.alphanums + '_-/.:')
1188
    quoted = dbl_quote + pyp.CharsNotIn('"') + dbl_quote
1189
    ipv4_addr = (pyp.Optional(pyp.Literal("ipv4")).suppress() +
1190
                 pyp.Word(pyp.nums + ".") + colon + number)
1191
    ipv6_addr = (pyp.Optional(pyp.Literal("ipv6")).suppress() +
1192
                 pyp.Optional(lbracket) + pyp.Word(pyp.hexnums + ":") +
1193
                 pyp.Optional(rbracket) + colon + number)
1194
    # meta device, extended syntax
1195
    meta_value = ((value ^ quoted) + lbracket + number + rbracket)
1196
    # device name, extended syntax
1197
    device_value = pyp.Literal("minor").suppress() + number
1198

    
1199
    # a statement
1200
    stmt = (~rbrace + keyword + ~lbrace +
1201
            pyp.Optional(ipv4_addr ^ ipv6_addr ^ value ^ quoted ^ meta_value ^
1202
                         device_value) +
1203
            pyp.Optional(defa) + semi +
1204
            pyp.Optional(pyp.restOfLine).suppress())
1205

    
1206
    # an entire section
1207
    section_name = pyp.Word(pyp.alphas + '_')
1208
    section = section_name + lbrace + pyp.ZeroOrMore(pyp.Group(stmt)) + rbrace
1209

    
1210
    bnf = pyp.ZeroOrMore(pyp.Group(section ^ stmt))
1211
    bnf.ignore(comment)
1212

    
1213
    cls._PARSE_SHOW = bnf
1214

    
1215
    return bnf
1216

    
1217
  @classmethod
1218
  def _GetShowData(cls, minor):
1219
    """Return the `drbdsetup show` data for a minor.
1220

1221
    """
1222
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "show"])
1223
    if result.failed:
1224
      logging.error("Can't display the drbd config: %s - %s",
1225
                    result.fail_reason, result.output)
1226
      return None
1227
    return result.stdout
1228

    
1229
  @classmethod
1230
  def _GetDevInfo(cls, out):
1231
    """Parse details about a given DRBD minor.
1232

1233
    This return, if available, the local backing device (as a path)
1234
    and the local and remote (ip, port) information from a string
1235
    containing the output of the `drbdsetup show` command as returned
1236
    by _GetShowData.
1237

1238
    """
1239
    data = {}
1240
    if not out:
1241
      return data
1242

    
1243
    bnf = cls._GetShowParser()
1244
    # run pyparse
1245

    
1246
    try:
1247
      results = bnf.parseString(out)
1248
    except pyp.ParseException, err:
1249
      _ThrowError("Can't parse drbdsetup show output: %s", str(err))
1250

    
1251
    # and massage the results into our desired format
1252
    for section in results:
1253
      sname = section[0]
1254
      if sname == "_this_host":
1255
        for lst in section[1:]:
1256
          if lst[0] == "disk":
1257
            data["local_dev"] = lst[1]
1258
          elif lst[0] == "meta-disk":
1259
            data["meta_dev"] = lst[1]
1260
            data["meta_index"] = lst[2]
1261
          elif lst[0] == "address":
1262
            data["local_addr"] = tuple(lst[1:])
1263
      elif sname == "_remote_host":
1264
        for lst in section[1:]:
1265
          if lst[0] == "address":
1266
            data["remote_addr"] = tuple(lst[1:])
1267
    return data
1268

    
1269
  def _MatchesLocal(self, info):
1270
    """Test if our local config matches with an existing device.
1271

1272
    The parameter should be as returned from `_GetDevInfo()`. This
1273
    method tests if our local backing device is the same as the one in
1274
    the info parameter, in effect testing if we look like the given
1275
    device.
1276

1277
    """
1278
    if self._children:
1279
      backend, meta = self._children
1280
    else:
1281
      backend = meta = None
1282

    
1283
    if backend is not None:
1284
      retval = ("local_dev" in info and info["local_dev"] == backend.dev_path)
1285
    else:
1286
      retval = ("local_dev" not in info)
1287

    
1288
    if meta is not None:
1289
      retval = retval and ("meta_dev" in info and
1290
                           info["meta_dev"] == meta.dev_path)
1291
      retval = retval and ("meta_index" in info and
1292
                           info["meta_index"] == 0)
1293
    else:
1294
      retval = retval and ("meta_dev" not in info and
1295
                           "meta_index" not in info)
1296
    return retval
1297

    
1298
  def _MatchesNet(self, info):
1299
    """Test if our network config matches with an existing device.
1300

1301
    The parameter should be as returned from `_GetDevInfo()`. This
1302
    method tests if our network configuration is the same as the one
1303
    in the info parameter, in effect testing if we look like the given
1304
    device.
1305

1306
    """
1307
    if (((self._lhost is None and not ("local_addr" in info)) and
1308
         (self._rhost is None and not ("remote_addr" in info)))):
1309
      return True
1310

    
1311
    if self._lhost is None:
1312
      return False
1313

    
1314
    if not ("local_addr" in info and
1315
            "remote_addr" in info):
1316
      return False
1317

    
1318
    retval = (info["local_addr"] == (self._lhost, self._lport))
1319
    retval = (retval and
1320
              info["remote_addr"] == (self._rhost, self._rport))
1321
    return retval
1322

    
1323
  @classmethod
1324
  def _AssembleLocal(cls, minor, backend, meta, size):
1325
    """Configure the local part of a DRBD device.
1326

1327
    """
1328
    args = ["drbdsetup", cls._DevPath(minor), "disk",
1329
            backend, meta, "0",
1330
            "-e", "detach",
1331
            "--create-device"]
1332
    if size:
1333
      args.extend(["-d", "%sm" % size])
1334
    if not constants.DRBD_BARRIERS: # disable barriers, if configured so
1335
      version = cls._GetVersion(cls._GetProcData())
1336
      # various DRBD versions support different disk barrier options;
1337
      # what we aim here is to revert back to the 'drain' method of
1338
      # disk flushes and to disable metadata barriers, in effect going
1339
      # back to pre-8.0.7 behaviour
1340
      vmaj = version['k_major']
1341
      vmin = version['k_minor']
1342
      vrel = version['k_point']
1343
      assert vmaj == 8
1344
      if vmin == 0: # 8.0.x
1345
        if vrel >= 12:
1346
          args.extend(['-i', '-m'])
1347
      elif vmin == 2: # 8.2.x
1348
        if vrel >= 7:
1349
          args.extend(['-i', '-m'])
1350
      elif vmaj >= 3: # 8.3.x or newer
1351
        args.extend(['-i', '-a', 'm'])
1352
    result = utils.RunCmd(args)
1353
    if result.failed:
1354
      _ThrowError("drbd%d: can't attach local disk: %s", minor, result.output)
1355

    
1356
  @classmethod
1357
  def _AssembleNet(cls, minor, net_info, protocol,
1358
                   dual_pri=False, hmac=None, secret=None):
1359
    """Configure the network part of the device.
1360

1361
    """
1362
    lhost, lport, rhost, rport = net_info
1363
    if None in net_info:
1364
      # we don't want network connection and actually want to make
1365
      # sure its shutdown
1366
      cls._ShutdownNet(minor)
1367
      return
1368

    
1369
    # Workaround for a race condition. When DRBD is doing its dance to
1370
    # establish a connection with its peer, it also sends the
1371
    # synchronization speed over the wire. In some cases setting the
1372
    # sync speed only after setting up both sides can race with DRBD
1373
    # connecting, hence we set it here before telling DRBD anything
1374
    # about its peer.
1375
    cls._SetMinorSyncSpeed(minor, constants.SYNC_SPEED)
1376

    
1377
    if netutils.IP6Address.IsValid(lhost):
1378
      if not netutils.IP6Address.IsValid(rhost):
1379
        _ThrowError("drbd%d: can't connect ip %s to ip %s" %
1380
                    (minor, lhost, rhost))
1381
      family = "ipv6"
1382
    elif netutils.IP4Address.IsValid(lhost):
1383
      if not netutils.IP4Address.IsValid(rhost):
1384
        _ThrowError("drbd%d: can't connect ip %s to ip %s" %
1385
                    (minor, lhost, rhost))
1386
      family = "ipv4"
1387
    else:
1388
      _ThrowError("drbd%d: Invalid ip %s" % (minor, lhost))
1389

    
1390
    args = ["drbdsetup", cls._DevPath(minor), "net",
1391
            "%s:%s:%s" % (family, lhost, lport),
1392
            "%s:%s:%s" % (family, rhost, rport), protocol,
1393
            "-A", "discard-zero-changes",
1394
            "-B", "consensus",
1395
            "--create-device",
1396
            ]
1397
    if dual_pri:
1398
      args.append("-m")
1399
    if hmac and secret:
1400
      args.extend(["-a", hmac, "-x", secret])
1401
    result = utils.RunCmd(args)
1402
    if result.failed:
1403
      _ThrowError("drbd%d: can't setup network: %s - %s",
1404
                  minor, result.fail_reason, result.output)
1405

    
1406
    def _CheckNetworkConfig():
1407
      info = cls._GetDevInfo(cls._GetShowData(minor))
1408
      if not "local_addr" in info or not "remote_addr" in info:
1409
        raise utils.RetryAgain()
1410

    
1411
      if (info["local_addr"] != (lhost, lport) or
1412
          info["remote_addr"] != (rhost, rport)):
1413
        raise utils.RetryAgain()
1414

    
1415
    try:
1416
      utils.Retry(_CheckNetworkConfig, 1.0, 10.0)
1417
    except utils.RetryTimeout:
1418
      _ThrowError("drbd%d: timeout while configuring network", minor)
1419

    
1420
  def AddChildren(self, devices):
1421
    """Add a disk to the DRBD device.
1422

1423
    """
1424
    if self.minor is None:
1425
      _ThrowError("drbd%d: can't attach to dbrd8 during AddChildren",
1426
                  self._aminor)
1427
    if len(devices) != 2:
1428
      _ThrowError("drbd%d: need two devices for AddChildren", self.minor)
1429
    info = self._GetDevInfo(self._GetShowData(self.minor))
1430
    if "local_dev" in info:
1431
      _ThrowError("drbd%d: already attached to a local disk", self.minor)
1432
    backend, meta = devices
1433
    if backend.dev_path is None or meta.dev_path is None:
1434
      _ThrowError("drbd%d: children not ready during AddChildren", self.minor)
1435
    backend.Open()
1436
    meta.Open()
1437
    self._CheckMetaSize(meta.dev_path)
1438
    self._InitMeta(self._FindUnusedMinor(), meta.dev_path)
1439

    
1440
    self._AssembleLocal(self.minor, backend.dev_path, meta.dev_path, self.size)
1441
    self._children = devices
1442

    
1443
  def RemoveChildren(self, devices):
1444
    """Detach the drbd device from local storage.
1445

1446
    """
1447
    if self.minor is None:
1448
      _ThrowError("drbd%d: can't attach to drbd8 during RemoveChildren",
1449
                  self._aminor)
1450
    # early return if we don't actually have backing storage
1451
    info = self._GetDevInfo(self._GetShowData(self.minor))
1452
    if "local_dev" not in info:
1453
      return
1454
    if len(self._children) != 2:
1455
      _ThrowError("drbd%d: we don't have two children: %s", self.minor,
1456
                  self._children)
1457
    if self._children.count(None) == 2: # we don't actually have children :)
1458
      logging.warning("drbd%d: requested detach while detached", self.minor)
1459
      return
1460
    if len(devices) != 2:
1461
      _ThrowError("drbd%d: we need two children in RemoveChildren", self.minor)
1462
    for child, dev in zip(self._children, devices):
1463
      if dev != child.dev_path:
1464
        _ThrowError("drbd%d: mismatch in local storage (%s != %s) in"
1465
                    " RemoveChildren", self.minor, dev, child.dev_path)
1466

    
1467
    self._ShutdownLocal(self.minor)
1468
    self._children = []
1469

    
1470
  @classmethod
1471
  def _SetMinorSyncSpeed(cls, minor, kbytes):
1472
    """Set the speed of the DRBD syncer.
1473

1474
    This is the low-level implementation.
1475

1476
    @type minor: int
1477
    @param minor: the drbd minor whose settings we change
1478
    @type kbytes: int
1479
    @param kbytes: the speed in kbytes/second
1480
    @rtype: boolean
1481
    @return: the success of the operation
1482

1483
    """
1484
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "syncer",
1485
                           "-r", "%d" % kbytes, "--create-device"])
1486
    if result.failed:
1487
      logging.error("Can't change syncer rate: %s - %s",
1488
                    result.fail_reason, result.output)
1489
    return not result.failed
1490

    
1491
  def SetSyncSpeed(self, kbytes):
1492
    """Set the speed of the DRBD syncer.
1493

1494
    @type kbytes: int
1495
    @param kbytes: the speed in kbytes/second
1496
    @rtype: boolean
1497
    @return: the success of the operation
1498

1499
    """
1500
    if self.minor is None:
1501
      logging.info("Not attached during SetSyncSpeed")
1502
      return False
1503
    children_result = super(DRBD8, self).SetSyncSpeed(kbytes)
1504
    return self._SetMinorSyncSpeed(self.minor, kbytes) and children_result
1505

    
1506
  def PauseResumeSync(self, pause):
1507
    """Pauses or resumes the sync of a DRBD device.
1508

1509
    @param pause: Wether to pause or resume
1510
    @return: the success of the operation
1511

1512
    """
1513
    if self.minor is None:
1514
      logging.info("Not attached during PauseSync")
1515
      return False
1516

    
1517
    children_result = super(DRBD8, self).PauseResumeSync(pause)
1518

    
1519
    if pause:
1520
      cmd = "pause-sync"
1521
    else:
1522
      cmd = "resume-sync"
1523

    
1524
    result = utils.RunCmd(["drbdsetup", self.dev_path, cmd])
1525
    if result.failed:
1526
      logging.error("Can't %s: %s - %s", cmd,
1527
                    result.fail_reason, result.output)
1528
    return not result.failed and children_result
1529

    
1530
  def GetProcStatus(self):
1531
    """Return device data from /proc.
1532

1533
    """
1534
    if self.minor is None:
1535
      _ThrowError("drbd%d: GetStats() called while not attached", self._aminor)
1536
    proc_info = self._MassageProcData(self._GetProcData())
1537
    if self.minor not in proc_info:
1538
      _ThrowError("drbd%d: can't find myself in /proc", self.minor)
1539
    return DRBD8Status(proc_info[self.minor])
1540

    
1541
  def GetSyncStatus(self):
1542
    """Returns the sync status of the device.
1543

1544

1545
    If sync_percent is None, it means all is ok
1546
    If estimated_time is None, it means we can't estimate
1547
    the time needed, otherwise it's the time left in seconds.
1548

1549

1550
    We set the is_degraded parameter to True on two conditions:
1551
    network not connected or local disk missing.
1552

1553
    We compute the ldisk parameter based on whether we have a local
1554
    disk or not.
1555

1556
    @rtype: objects.BlockDevStatus
1557

1558
    """
1559
    if self.minor is None and not self.Attach():
1560
      _ThrowError("drbd%d: can't Attach() in GetSyncStatus", self._aminor)
1561

    
1562
    stats = self.GetProcStatus()
1563
    is_degraded = not stats.is_connected or not stats.is_disk_uptodate
1564

    
1565
    if stats.is_disk_uptodate:
1566
      ldisk_status = constants.LDS_OKAY
1567
    elif stats.is_diskless:
1568
      ldisk_status = constants.LDS_FAULTY
1569
    else:
1570
      ldisk_status = constants.LDS_UNKNOWN
1571

    
1572
    return objects.BlockDevStatus(dev_path=self.dev_path,
1573
                                  major=self.major,
1574
                                  minor=self.minor,
1575
                                  sync_percent=stats.sync_percent,
1576
                                  estimated_time=stats.est_time,
1577
                                  is_degraded=is_degraded,
1578
                                  ldisk_status=ldisk_status)
1579

    
1580
  def Open(self, force=False):
1581
    """Make the local state primary.
1582

1583
    If the 'force' parameter is given, the '-o' option is passed to
1584
    drbdsetup. Since this is a potentially dangerous operation, the
1585
    force flag should be only given after creation, when it actually
1586
    is mandatory.
1587

1588
    """
1589
    if self.minor is None and not self.Attach():
1590
      logging.error("DRBD cannot attach to a device during open")
1591
      return False
1592
    cmd = ["drbdsetup", self.dev_path, "primary"]
1593
    if force:
1594
      cmd.append("-o")
1595
    result = utils.RunCmd(cmd)
1596
    if result.failed:
1597
      _ThrowError("drbd%d: can't make drbd device primary: %s", self.minor,
1598
                  result.output)
1599

    
1600
  def Close(self):
1601
    """Make the local state secondary.
1602

1603
    This will, of course, fail if the device is in use.
1604

1605
    """
1606
    if self.minor is None and not self.Attach():
1607
      _ThrowError("drbd%d: can't Attach() in Close()", self._aminor)
1608
    result = utils.RunCmd(["drbdsetup", self.dev_path, "secondary"])
1609
    if result.failed:
1610
      _ThrowError("drbd%d: can't switch drbd device to secondary: %s",
1611
                  self.minor, result.output)
1612

    
1613
  def DisconnectNet(self):
1614
    """Removes network configuration.
1615

1616
    This method shutdowns the network side of the device.
1617

1618
    The method will wait up to a hardcoded timeout for the device to
1619
    go into standalone after the 'disconnect' command before
1620
    re-configuring it, as sometimes it takes a while for the
1621
    disconnect to actually propagate and thus we might issue a 'net'
1622
    command while the device is still connected. If the device will
1623
    still be attached to the network and we time out, we raise an
1624
    exception.
1625

1626
    """
1627
    if self.minor is None:
1628
      _ThrowError("drbd%d: disk not attached in re-attach net", self._aminor)
1629

    
1630
    if None in (self._lhost, self._lport, self._rhost, self._rport):
1631
      _ThrowError("drbd%d: DRBD disk missing network info in"
1632
                  " DisconnectNet()", self.minor)
1633

    
1634
    class _DisconnectStatus:
1635
      def __init__(self, ever_disconnected):
1636
        self.ever_disconnected = ever_disconnected
1637

    
1638
    dstatus = _DisconnectStatus(_IgnoreError(self._ShutdownNet, self.minor))
1639

    
1640
    def _WaitForDisconnect():
1641
      if self.GetProcStatus().is_standalone:
1642
        return
1643

    
1644
      # retry the disconnect, it seems possible that due to a well-time
1645
      # disconnect on the peer, my disconnect command might be ignored and
1646
      # forgotten
1647
      dstatus.ever_disconnected = \
1648
        _IgnoreError(self._ShutdownNet, self.minor) or dstatus.ever_disconnected
1649

    
1650
      raise utils.RetryAgain()
1651

    
1652
    # Keep start time
1653
    start_time = time.time()
1654

    
1655
    try:
1656
      # Start delay at 100 milliseconds and grow up to 2 seconds
1657
      utils.Retry(_WaitForDisconnect, (0.1, 1.5, 2.0),
1658
                  self._NET_RECONFIG_TIMEOUT)
1659
    except utils.RetryTimeout:
1660
      if dstatus.ever_disconnected:
1661
        msg = ("drbd%d: device did not react to the"
1662
               " 'disconnect' command in a timely manner")
1663
      else:
1664
        msg = "drbd%d: can't shutdown network, even after multiple retries"
1665

    
1666
      _ThrowError(msg, self.minor)
1667

    
1668
    reconfig_time = time.time() - start_time
1669
    if reconfig_time > (self._NET_RECONFIG_TIMEOUT * 0.25):
1670
      logging.info("drbd%d: DisconnectNet: detach took %.3f seconds",
1671
                   self.minor, reconfig_time)
1672

    
1673
  def AttachNet(self, multimaster):
1674
    """Reconnects the network.
1675

1676
    This method connects the network side of the device with a
1677
    specified multi-master flag. The device needs to be 'Standalone'
1678
    but have valid network configuration data.
1679

1680
    Args:
1681
      - multimaster: init the network in dual-primary mode
1682

1683
    """
1684
    if self.minor is None:
1685
      _ThrowError("drbd%d: device not attached in AttachNet", self._aminor)
1686

    
1687
    if None in (self._lhost, self._lport, self._rhost, self._rport):
1688
      _ThrowError("drbd%d: missing network info in AttachNet()", self.minor)
1689

    
1690
    status = self.GetProcStatus()
1691

    
1692
    if not status.is_standalone:
1693
      _ThrowError("drbd%d: device is not standalone in AttachNet", self.minor)
1694

    
1695
    self._AssembleNet(self.minor,
1696
                      (self._lhost, self._lport, self._rhost, self._rport),
1697
                      constants.DRBD_NET_PROTOCOL, dual_pri=multimaster,
1698
                      hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1699

    
1700
  def Attach(self):
1701
    """Check if our minor is configured.
1702

1703
    This doesn't do any device configurations - it only checks if the
1704
    minor is in a state different from Unconfigured.
1705

1706
    Note that this function will not change the state of the system in
1707
    any way (except in case of side-effects caused by reading from
1708
    /proc).
1709

1710
    """
1711
    used_devs = self.GetUsedDevs()
1712
    if self._aminor in used_devs:
1713
      minor = self._aminor
1714
    else:
1715
      minor = None
1716

    
1717
    self._SetFromMinor(minor)
1718
    return minor is not None
1719

    
1720
  def Assemble(self):
1721
    """Assemble the drbd.
1722

1723
    Method:
1724
      - if we have a configured device, we try to ensure that it matches
1725
        our config
1726
      - if not, we create it from zero
1727

1728
    """
1729
    super(DRBD8, self).Assemble()
1730

    
1731
    self.Attach()
1732
    if self.minor is None:
1733
      # local device completely unconfigured
1734
      self._FastAssemble()
1735
    else:
1736
      # we have to recheck the local and network status and try to fix
1737
      # the device
1738
      self._SlowAssemble()
1739

    
1740
  def _SlowAssemble(self):
1741
    """Assembles the DRBD device from a (partially) configured device.
1742

1743
    In case of partially attached (local device matches but no network
1744
    setup), we perform the network attach. If successful, we re-test
1745
    the attach if can return success.
1746

1747
    """
1748
    # TODO: Rewrite to not use a for loop just because there is 'break'
1749
    # pylint: disable-msg=W0631
1750
    net_data = (self._lhost, self._lport, self._rhost, self._rport)
1751
    for minor in (self._aminor,):
1752
      info = self._GetDevInfo(self._GetShowData(minor))
1753
      match_l = self._MatchesLocal(info)
1754
      match_r = self._MatchesNet(info)
1755

    
1756
      if match_l and match_r:
1757
        # everything matches
1758
        break
1759

    
1760
      if match_l and not match_r and "local_addr" not in info:
1761
        # disk matches, but not attached to network, attach and recheck
1762
        self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
1763
                          hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1764
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1765
          break
1766
        else:
1767
          _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
1768
                      " show' disagrees", minor)
1769

    
1770
      if match_r and "local_dev" not in info:
1771
        # no local disk, but network attached and it matches
1772
        self._AssembleLocal(minor, self._children[0].dev_path,
1773
                            self._children[1].dev_path, self.size)
1774
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1775
          break
1776
        else:
1777
          _ThrowError("drbd%d: disk attach successful, but 'drbdsetup"
1778
                      " show' disagrees", minor)
1779

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

    
1807
    else:
1808
      minor = None
1809

    
1810
    self._SetFromMinor(minor)
1811
    if minor is None:
1812
      _ThrowError("drbd%d: cannot activate, unknown or unhandled reason",
1813
                  self._aminor)
1814

    
1815
  def _FastAssemble(self):
1816
    """Assemble the drbd device from zero.
1817

1818
    This is run when in Assemble we detect our minor is unused.
1819

1820
    """
1821
    minor = self._aminor
1822
    if self._children and self._children[0] and self._children[1]:
1823
      self._AssembleLocal(minor, self._children[0].dev_path,
1824
                          self._children[1].dev_path, self.size)
1825
    if self._lhost and self._lport and self._rhost and self._rport:
1826
      self._AssembleNet(minor,
1827
                        (self._lhost, self._lport, self._rhost, self._rport),
1828
                        constants.DRBD_NET_PROTOCOL,
1829
                        hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1830
    self._SetFromMinor(minor)
1831

    
1832
  @classmethod
1833
  def _ShutdownLocal(cls, minor):
1834
    """Detach from the local device.
1835

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

1839
    """
1840
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "detach"])
1841
    if result.failed:
1842
      _ThrowError("drbd%d: can't detach local disk: %s", minor, result.output)
1843

    
1844
  @classmethod
1845
  def _ShutdownNet(cls, minor):
1846
    """Disconnect from the remote peer.
1847

1848
    This fails if we don't have a local device.
1849

1850
    """
1851
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disconnect"])
1852
    if result.failed:
1853
      _ThrowError("drbd%d: can't shutdown network: %s", minor, result.output)
1854

    
1855
  @classmethod
1856
  def _ShutdownAll(cls, minor):
1857
    """Deactivate the device.
1858

1859
    This will, of course, fail if the device is in use.
1860

1861
    """
1862
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "down"])
1863
    if result.failed:
1864
      _ThrowError("drbd%d: can't shutdown drbd device: %s",
1865
                  minor, result.output)
1866

    
1867
  def Shutdown(self):
1868
    """Shutdown the DRBD device.
1869

1870
    """
1871
    if self.minor is None and not self.Attach():
1872
      logging.info("drbd%d: not attached during Shutdown()", self._aminor)
1873
      return
1874
    minor = self.minor
1875
    self.minor = None
1876
    self.dev_path = None
1877
    self._ShutdownAll(minor)
1878

    
1879
  def Remove(self):
1880
    """Stub remove for DRBD devices.
1881

1882
    """
1883
    self.Shutdown()
1884

    
1885
  @classmethod
1886
  def Create(cls, unique_id, children, size):
1887
    """Create a new DRBD8 device.
1888

1889
    Since DRBD devices are not created per se, just assembled, this
1890
    function only initializes the metadata.
1891

1892
    """
1893
    if len(children) != 2:
1894
      raise errors.ProgrammerError("Invalid setup for the drbd device")
1895
    # check that the minor is unused
1896
    aminor = unique_id[4]
1897
    proc_info = cls._MassageProcData(cls._GetProcData())
1898
    if aminor in proc_info:
1899
      status = DRBD8Status(proc_info[aminor])
1900
      in_use = status.is_in_use
1901
    else:
1902
      in_use = False
1903
    if in_use:
1904
      _ThrowError("drbd%d: minor is already in use at Create() time", aminor)
1905
    meta = children[1]
1906
    meta.Assemble()
1907
    if not meta.Attach():
1908
      _ThrowError("drbd%d: can't attach to meta device '%s'",
1909
                  aminor, meta)
1910
    cls._CheckMetaSize(meta.dev_path)
1911
    cls._InitMeta(aminor, meta.dev_path)
1912
    return cls(unique_id, children, size)
1913

    
1914
  def Grow(self, amount):
1915
    """Resize the DRBD device and its backing storage.
1916

1917
    """
1918
    if self.minor is None:
1919
      _ThrowError("drbd%d: Grow called while not attached", self._aminor)
1920
    if len(self._children) != 2 or None in self._children:
1921
      _ThrowError("drbd%d: cannot grow diskless device", self.minor)
1922
    self._children[0].Grow(amount)
1923
    result = utils.RunCmd(["drbdsetup", self.dev_path, "resize", "-s",
1924
                           "%dm" % (self.size + amount)])
1925
    if result.failed:
1926
      _ThrowError("drbd%d: resize failed: %s", self.minor, result.output)
1927

    
1928

    
1929
class FileStorage(BlockDev):
1930
  """File device.
1931

1932
  This class represents the a file storage backend device.
1933

1934
  The unique_id for the file device is a (file_driver, file_path) tuple.
1935

1936
  """
1937
  def __init__(self, unique_id, children, size):
1938
    """Initalizes a file device backend.
1939

1940
    """
1941
    if children:
1942
      raise errors.BlockDeviceError("Invalid setup for file device")
1943
    super(FileStorage, self).__init__(unique_id, children, size)
1944
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1945
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1946
    self.driver = unique_id[0]
1947
    self.dev_path = unique_id[1]
1948
    self.Attach()
1949

    
1950
  def Assemble(self):
1951
    """Assemble the device.
1952

1953
    Checks whether the file device exists, raises BlockDeviceError otherwise.
1954

1955
    """
1956
    if not os.path.exists(self.dev_path):
1957
      _ThrowError("File device '%s' does not exist" % self.dev_path)
1958

    
1959
  def Shutdown(self):
1960
    """Shutdown the device.
1961

1962
    This is a no-op for the file type, as we don't deactivate
1963
    the file on shutdown.
1964

1965
    """
1966
    pass
1967

    
1968
  def Open(self, force=False):
1969
    """Make the device ready for I/O.
1970

1971
    This is a no-op for the file type.
1972

1973
    """
1974
    pass
1975

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

1979
    This is a no-op for the file type.
1980

1981
    """
1982
    pass
1983

    
1984
  def Remove(self):
1985
    """Remove the file backing the block device.
1986

1987
    @rtype: boolean
1988
    @return: True if the removal was successful
1989

1990
    """
1991
    try:
1992
      os.remove(self.dev_path)
1993
    except OSError, err:
1994
      if err.errno != errno.ENOENT:
1995
        _ThrowError("Can't remove file '%s': %s", self.dev_path, err)
1996

    
1997
  def Rename(self, new_id):
1998
    """Renames the file.
1999

2000
    """
2001
    # TODO: implement rename for file-based storage
2002
    _ThrowError("Rename is not supported for file-based storage")
2003

    
2004
  def Grow(self, amount):
2005
    """Grow the file
2006

2007
    @param amount: the amount (in mebibytes) to grow with
2008

2009
    """
2010
    # Check that the file exists
2011
    self.Assemble()
2012
    current_size = self.GetActualSize()
2013
    new_size = current_size + amount * 1024 * 1024
2014
    assert new_size > current_size, "Cannot Grow with a negative amount"
2015
    try:
2016
      f = open(self.dev_path, "a+")
2017
      f.truncate(new_size)
2018
      f.close()
2019
    except EnvironmentError, err:
2020
      _ThrowError("Error in file growth: %", str(err))
2021

    
2022
  def Attach(self):
2023
    """Attach to an existing file.
2024

2025
    Check if this file already exists.
2026

2027
    @rtype: boolean
2028
    @return: True if file exists
2029

2030
    """
2031
    self.attached = os.path.exists(self.dev_path)
2032
    return self.attached
2033

    
2034
  def GetActualSize(self):
2035
    """Return the actual disk size.
2036

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

2039
    """
2040
    assert self.attached, "BlockDevice not attached in GetActualSize()"
2041
    try:
2042
      st = os.stat(self.dev_path)
2043
      return st.st_size
2044
    except OSError, err:
2045
      _ThrowError("Can't stat %s: %s", self.dev_path, err)
2046

    
2047
  @classmethod
2048
  def Create(cls, unique_id, children, size):
2049
    """Create a new file.
2050

2051
    @param size: the size of file in MiB
2052

2053
    @rtype: L{bdev.FileStorage}
2054
    @return: an instance of FileStorage
2055

2056
    """
2057
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
2058
      raise ValueError("Invalid configuration data %s" % str(unique_id))
2059
    dev_path = unique_id[1]
2060
    try:
2061
      fd = os.open(dev_path, os.O_RDWR | os.O_CREAT | os.O_EXCL)
2062
      f = os.fdopen(fd, "w")
2063
      f.truncate(size * 1024 * 1024)
2064
      f.close()
2065
    except EnvironmentError, err:
2066
      if err.errno == errno.EEXIST:
2067
        _ThrowError("File already existing: %s", dev_path)
2068
      _ThrowError("Error in file creation: %", str(err))
2069

    
2070
    return FileStorage(unique_id, children, size)
2071

    
2072

    
2073
class PersistentBlockDevice(BlockDev):
2074
  """A block device with persistent node
2075

2076
  May be either directly attached, or exposed through DM (e.g. dm-multipath).
2077
  udev helpers are probably required to give persistent, human-friendly
2078
  names.
2079

2080
  For the time being, pathnames are required to lie under /dev.
2081

2082
  """
2083
  def __init__(self, unique_id, children, size):
2084
    """Attaches to a static block device.
2085

2086
    The unique_id is a path under /dev.
2087

2088
    """
2089
    super(PersistentBlockDevice, self).__init__(unique_id, children, size)
2090
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
2091
      raise ValueError("Invalid configuration data %s" % str(unique_id))
2092
    self.dev_path = unique_id[1]
2093
    if not os.path.realpath(self.dev_path).startswith('/dev/'):
2094
      raise ValueError("Full path '%s' lies outside /dev" %
2095
                              os.path.realpath(self.dev_path))
2096
    # TODO: this is just a safety guard checking that we only deal with devices
2097
    # we know how to handle. In the future this will be integrated with
2098
    # external storage backends and possible values will probably be collected
2099
    # from the cluster configuration.
2100
    if unique_id[0] != constants.BLOCKDEV_DRIVER_MANUAL:
2101
      raise ValueError("Got persistent block device of invalid type: %s" %
2102
                       unique_id[0])
2103

    
2104
    self.major = self.minor = None
2105
    self.Attach()
2106

    
2107
  @classmethod
2108
  def Create(cls, unique_id, children, size):
2109
    """Create a new device
2110

2111
    This is a noop, we only return a PersistentBlockDevice instance
2112

2113
    """
2114
    return PersistentBlockDevice(unique_id, children, 0)
2115

    
2116
  def Remove(self):
2117
    """Remove a device
2118

2119
    This is a noop
2120

2121
    """
2122
    pass
2123

    
2124
  def Rename(self, new_id):
2125
    """Rename this device.
2126

2127
    """
2128
    _ThrowError("Rename is not supported for PersistentBlockDev storage")
2129

    
2130
  def Attach(self):
2131
    """Attach to an existing block device.
2132

2133

2134
    """
2135
    self.attached = False
2136
    try:
2137
      st = os.stat(self.dev_path)
2138
    except OSError, err:
2139
      logging.error("Error stat()'ing %s: %s", self.dev_path, str(err))
2140
      return False
2141

    
2142
    if not stat.S_ISBLK(st.st_mode):
2143
      logging.error("%s is not a block device", self.dev_path)
2144
      return False
2145

    
2146
    self.major = os.major(st.st_rdev)
2147
    self.minor = os.minor(st.st_rdev)
2148
    self.attached = True
2149

    
2150
    return True
2151

    
2152
  def Assemble(self):
2153
    """Assemble the device.
2154

2155
    """
2156
    pass
2157

    
2158
  def Shutdown(self):
2159
    """Shutdown the device.
2160

2161
    """
2162
    pass
2163

    
2164
  def Open(self, force=False):
2165
    """Make the device ready for I/O.
2166

2167
    """
2168
    pass
2169

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

2173
    """
2174
    pass
2175

    
2176
  def Grow(self, amount):
2177
    """Grow the logical volume.
2178

2179
    """
2180
    _ThrowError("Grow is not supported for PersistentBlockDev storage")
2181

    
2182

    
2183
DEV_MAP = {
2184
  constants.LD_LV: LogicalVolume,
2185
  constants.LD_DRBD8: DRBD8,
2186
  constants.LD_BLOCKDEV: PersistentBlockDevice,
2187
  }
2188

    
2189
if constants.ENABLE_FILE_STORAGE or constants.ENABLE_SHARED_FILE_STORAGE:
2190
  DEV_MAP[constants.LD_FILE] = FileStorage
2191

    
2192

    
2193
def FindDevice(dev_type, unique_id, children, size):
2194
  """Search for an existing, assembled device.
2195

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

2199
  """
2200
  if dev_type not in DEV_MAP:
2201
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
2202
  device = DEV_MAP[dev_type](unique_id, children, size)
2203
  if not device.attached:
2204
    return None
2205
  return device
2206

    
2207

    
2208
def Assemble(dev_type, unique_id, children, size):
2209
  """Try to attach or assemble an existing device.
2210

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

2214
  """
2215
  if dev_type not in DEV_MAP:
2216
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
2217
  device = DEV_MAP[dev_type](unique_id, children, size)
2218
  device.Assemble()
2219
  return device
2220

    
2221

    
2222
def Create(dev_type, unique_id, children, size):
2223
  """Create a device.
2224

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