Statistics
| Branch: | Tag: | Revision:

root / lib / bdev.py @ 94dcbdb0

History | View | Annotate | Download (73.1 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, params):
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
    self.params = params
142

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

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

154
    """
155
    pass
156

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

160
    """
161
    raise NotImplementedError
162

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

166
    """
167
    raise NotImplementedError
168

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

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

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

180
    """
181
    raise NotImplementedError
182

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

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

190
    """
191
    raise NotImplementedError
192

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

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

198
    """
199
    raise NotImplementedError
200

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

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

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

210
    """
211
    raise NotImplementedError
212

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

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

220
    """
221
    raise NotImplementedError
222

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

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

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

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

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

240
    @param pause: Wheater to pause or resume
241

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

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

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

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

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

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

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

268
    @rtype: objects.BlockDevStatus
269

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

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

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

286
    @rtype: objects.BlockDevStatus
287

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

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

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

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

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

    
310
        is_degraded = is_degraded or child_status.is_degraded
311

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

    
317
    return objects.BlockDevStatus(dev_path=self.dev_path,
318
                                  major=self.major,
319
                                  minor=self.minor,
320
                                  sync_percent=min_percent,
321
                                  estimated_time=max_time,
322
                                  is_degraded=is_degraded,
323
                                  ldisk_status=ldisk_status)
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, params):
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, params)
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, params):
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, params)
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, self.params)
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
                       # Due to a bug in drbd in the kernel, introduced in
796
                       # commit 4b0715f096 (still unfixed as of 2011-08-22)
797
                       "(?:\s|M)"
798
                       "finish: ([0-9]+):([0-9]+):([0-9]+)\s.*$")
799

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

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

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

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

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

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

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

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

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

    
892

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
986
    return retval
987

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

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

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

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

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

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

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

    
1030
    return used_devs
1031

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

1035
    This sets our minor variable and our dev_path.
1036

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

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

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

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

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

1077
    This is not supported for drbd devices.
1078

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

    
1082

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

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

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

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

    
1099
  # timeout constants
1100
  _NET_RECONFIG_TIMEOUT = 60
1101

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1233
    cls._PARSE_SHOW = bnf
1234

    
1235
    return bnf
1236

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1494
    This is the low-level implementation.
1495

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1564

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

1569

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

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

1576
    @rtype: objects.BlockDevStatus
1577

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1670
      raise utils.RetryAgain()
1671

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

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

    
1686
      _ThrowError(msg, self.minor)
1687

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

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

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

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

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

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

    
1710
    status = self.GetProcStatus()
1711

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

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

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

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

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

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

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

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

1743
    Method:
1744
      - if we have a configured device, we try to ensure that it matches
1745
        our config
1746
      - if not, we create it from zero
1747
      - anyway, set the device parameters
1748

1749
    """
1750
    super(DRBD8, self).Assemble()
1751

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

    
1761
    self.SetSyncSpeed(constants.SYNC_SPEED)
1762

    
1763
  def _SlowAssemble(self):
1764
    """Assembles the DRBD device from a (partially) configured device.
1765

1766
    In case of partially attached (local device matches but no network
1767
    setup), we perform the network attach. If successful, we re-test
1768
    the attach if can return success.
1769

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

    
1779
      if match_l and match_r:
1780
        # everything matches
1781
        break
1782

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

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

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

    
1830
    else:
1831
      minor = None
1832

    
1833
    self._SetFromMinor(minor)
1834
    if minor is None:
1835
      _ThrowError("drbd%d: cannot activate, unknown or unhandled reason",
1836
                  self._aminor)
1837

    
1838
  def _FastAssemble(self):
1839
    """Assemble the drbd device from zero.
1840

1841
    This is run when in Assemble we detect our minor is unused.
1842

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

    
1855
  @classmethod
1856
  def _ShutdownLocal(cls, minor):
1857
    """Detach from the local device.
1858

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

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

    
1867
  @classmethod
1868
  def _ShutdownNet(cls, minor):
1869
    """Disconnect from the remote peer.
1870

1871
    This fails if we don't have a local device.
1872

1873
    """
1874
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disconnect"])
1875
    if result.failed:
1876
      _ThrowError("drbd%d: can't shutdown network: %s", minor, result.output)
1877

    
1878
  @classmethod
1879
  def _ShutdownAll(cls, minor):
1880
    """Deactivate the device.
1881

1882
    This will, of course, fail if the device is in use.
1883

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

    
1890
  def Shutdown(self):
1891
    """Shutdown the DRBD device.
1892

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

    
1902
  def Remove(self):
1903
    """Stub remove for DRBD devices.
1904

1905
    """
1906
    self.Shutdown()
1907

    
1908
  @classmethod
1909
  def Create(cls, unique_id, children, size, params):
1910
    """Create a new DRBD8 device.
1911

1912
    Since DRBD devices are not created per se, just assembled, this
1913
    function only initializes the metadata.
1914

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

    
1937
  def Grow(self, amount, dryrun):
1938
    """Resize the DRBD device and its backing storage.
1939

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

    
1954

    
1955
class FileStorage(BlockDev):
1956
  """File device.
1957

1958
  This class represents the a file storage backend device.
1959

1960
  The unique_id for the file device is a (file_driver, file_path) tuple.
1961

1962
  """
1963
  def __init__(self, unique_id, children, size, params):
1964
    """Initalizes a file device backend.
1965

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

    
1976
  def Assemble(self):
1977
    """Assemble the device.
1978

1979
    Checks whether the file device exists, raises BlockDeviceError otherwise.
1980

1981
    """
1982
    if not os.path.exists(self.dev_path):
1983
      _ThrowError("File device '%s' does not exist" % self.dev_path)
1984

    
1985
  def Shutdown(self):
1986
    """Shutdown the device.
1987

1988
    This is a no-op for the file type, as we don't deactivate
1989
    the file on shutdown.
1990

1991
    """
1992
    pass
1993

    
1994
  def Open(self, force=False):
1995
    """Make the device ready for I/O.
1996

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

1999
    """
2000
    pass
2001

    
2002
  def Close(self):
2003
    """Notifies that the device will no longer be used for I/O.
2004

2005
    This is a no-op for the file type.
2006

2007
    """
2008
    pass
2009

    
2010
  def Remove(self):
2011
    """Remove the file backing the block device.
2012

2013
    @rtype: boolean
2014
    @return: True if the removal was successful
2015

2016
    """
2017
    try:
2018
      os.remove(self.dev_path)
2019
    except OSError, err:
2020
      if err.errno != errno.ENOENT:
2021
        _ThrowError("Can't remove file '%s': %s", self.dev_path, err)
2022

    
2023
  def Rename(self, new_id):
2024
    """Renames the file.
2025

2026
    """
2027
    # TODO: implement rename for file-based storage
2028
    _ThrowError("Rename is not supported for file-based storage")
2029

    
2030
  def Grow(self, amount, dryrun):
2031
    """Grow the file
2032

2033
    @param amount: the amount (in mebibytes) to grow with
2034

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

    
2051
  def Attach(self):
2052
    """Attach to an existing file.
2053

2054
    Check if this file already exists.
2055

2056
    @rtype: boolean
2057
    @return: True if file exists
2058

2059
    """
2060
    self.attached = os.path.exists(self.dev_path)
2061
    return self.attached
2062

    
2063
  def GetActualSize(self):
2064
    """Return the actual disk size.
2065

2066
    @note: the device needs to be active when this is called
2067

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

    
2076
  @classmethod
2077
  def Create(cls, unique_id, children, size, params):
2078
    """Create a new file.
2079

2080
    @param size: the size of file in MiB
2081

2082
    @rtype: L{bdev.FileStorage}
2083
    @return: an instance of FileStorage
2084

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

    
2099
    return FileStorage(unique_id, children, size, params)
2100

    
2101

    
2102
class PersistentBlockDevice(BlockDev):
2103
  """A block device with persistent node
2104

2105
  May be either directly attached, or exposed through DM (e.g. dm-multipath).
2106
  udev helpers are probably required to give persistent, human-friendly
2107
  names.
2108

2109
  For the time being, pathnames are required to lie under /dev.
2110

2111
  """
2112
  def __init__(self, unique_id, children, size, params):
2113
    """Attaches to a static block device.
2114

2115
    The unique_id is a path under /dev.
2116

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

    
2134
    self.major = self.minor = None
2135
    self.Attach()
2136

    
2137
  @classmethod
2138
  def Create(cls, unique_id, children, size, params):
2139
    """Create a new device
2140

2141
    This is a noop, we only return a PersistentBlockDevice instance
2142

2143
    """
2144
    return PersistentBlockDevice(unique_id, children, 0, params)
2145

    
2146
  def Remove(self):
2147
    """Remove a device
2148

2149
    This is a noop
2150

2151
    """
2152
    pass
2153

    
2154
  def Rename(self, new_id):
2155
    """Rename this device.
2156

2157
    """
2158
    _ThrowError("Rename is not supported for PersistentBlockDev storage")
2159

    
2160
  def Attach(self):
2161
    """Attach to an existing block device.
2162

2163

2164
    """
2165
    self.attached = False
2166
    try:
2167
      st = os.stat(self.dev_path)
2168
    except OSError, err:
2169
      logging.error("Error stat()'ing %s: %s", self.dev_path, str(err))
2170
      return False
2171

    
2172
    if not stat.S_ISBLK(st.st_mode):
2173
      logging.error("%s is not a block device", self.dev_path)
2174
      return False
2175

    
2176
    self.major = os.major(st.st_rdev)
2177
    self.minor = os.minor(st.st_rdev)
2178
    self.attached = True
2179

    
2180
    return True
2181

    
2182
  def Assemble(self):
2183
    """Assemble the device.
2184

2185
    """
2186
    pass
2187

    
2188
  def Shutdown(self):
2189
    """Shutdown the device.
2190

2191
    """
2192
    pass
2193

    
2194
  def Open(self, force=False):
2195
    """Make the device ready for I/O.
2196

2197
    """
2198
    pass
2199

    
2200
  def Close(self):
2201
    """Notifies that the device will no longer be used for I/O.
2202

2203
    """
2204
    pass
2205

    
2206
  def Grow(self, amount, dryrun):
2207
    """Grow the logical volume.
2208

2209
    """
2210
    _ThrowError("Grow is not supported for PersistentBlockDev storage")
2211

    
2212

    
2213
DEV_MAP = {
2214
  constants.LD_LV: LogicalVolume,
2215
  constants.LD_DRBD8: DRBD8,
2216
  constants.LD_BLOCKDEV: PersistentBlockDevice,
2217
  }
2218

    
2219
if constants.ENABLE_FILE_STORAGE or constants.ENABLE_SHARED_FILE_STORAGE:
2220
  DEV_MAP[constants.LD_FILE] = FileStorage
2221

    
2222

    
2223
def _VerifyDiskType(dev_type):
2224
  if dev_type not in DEV_MAP:
2225
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
2226

    
2227

    
2228
def FindDevice(disk, children):
2229
  """Search for an existing, assembled device.
2230

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

2234
  @type disk: L{objects.Disk}
2235
  @param disk: the disk object to find
2236
  @type children: list of L{bdev.BlockDev}
2237
  @param children: the list of block devices that are children of the device
2238
                  represented by the disk parameter
2239

2240
  """
2241
  _VerifyDiskType(disk.dev_type)
2242
  dev_params = objects.FillDict(constants.DISK_LD_DEFAULTS[disk.dev_type],
2243
                                disk.params)
2244
  device = DEV_MAP[disk.dev_type](disk.physical_id, children, disk.size,
2245
                                  dev_params)
2246
  if not device.attached:
2247
    return None
2248
  return device
2249

    
2250

    
2251
def Assemble(disk, children):
2252
  """Try to attach or assemble an existing device.
2253

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

2257
  @type disk: L{objects.Disk}
2258
  @param disk: the disk object to assemble
2259
  @type children: list of L{bdev.BlockDev}
2260
  @param children: the list of block devices that are children of the device
2261
                  represented by the disk parameter
2262

2263
  """
2264
  _VerifyDiskType(disk.dev_type)
2265
  dev_params = objects.FillDict(constants.DISK_LD_DEFAULTS[disk.dev_type],
2266
                                disk.params)
2267
  device = DEV_MAP[disk.dev_type](disk.physical_id, children, disk.size,
2268
                                  dev_params)
2269
  device.Assemble()
2270
  return device
2271

    
2272

    
2273
def Create(disk, children):
2274
  """Create a device.
2275

2276
  @type disk: L{objects.Disk}
2277
  @param disk: the disk object to create
2278
  @type children: list of L{bdev.BlockDev}
2279
  @param children: the list of block devices that are children of the device
2280
                  represented by the disk parameter
2281

2282
  """
2283
  _VerifyDiskType(disk.dev_type)
2284
  dev_params = objects.FillDict(constants.DISK_LD_DEFAULTS[disk.dev_type],
2285
                                disk.params)
2286
  device = DEV_MAP[disk.dev_type].Create(disk.physical_id, children, disk.size,
2287
                                         dev_params)
2288
  return device