Statistics
| Branch: | Tag: | Revision:

root / lib / bdev.py @ 6e9814a1

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
  def _AssembleNet(self, minor, net_info, protocol,
1377
                   dual_pri=False, hmac=None, secret=None):
1378
    """Configure the network part of the device.
1379

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

    
1388
    # Workaround for a race condition. When DRBD is doing its dance to
1389
    # establish a connection with its peer, it also sends the
1390
    # synchronization speed over the wire. In some cases setting the
1391
    # sync speed only after setting up both sides can race with DRBD
1392
    # connecting, hence we set it here before telling DRBD anything
1393
    # about its peer.
1394
    sync_speed = self.params.get(constants.RESYNC_RATE)
1395
    self._SetMinorSyncSpeed(minor, 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", self._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 = self._GetDevInfo(self._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
    sync_speed = self.params.get(constants.RESYNC_RATE)
1762
    self.SetSyncSpeed(sync_speed)
1763

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

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

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

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

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

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

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

    
1831
    else:
1832
      minor = None
1833

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1906
    """
1907
    self.Shutdown()
1908

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

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

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

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

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

    
1955

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

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

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

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

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

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

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

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

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

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

1992
    """
1993
    pass
1994

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

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

2000
    """
2001
    pass
2002

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

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

2008
    """
2009
    pass
2010

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

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

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

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

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

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

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

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

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

2055
    Check if this file already exists.
2056

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

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

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

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

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

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

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

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

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

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

    
2102

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

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

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

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

2116
    The unique_id is a path under /dev.
2117

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

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

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

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

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

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

2150
    This is a noop
2151

2152
    """
2153
    pass
2154

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

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

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

2164

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

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

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

    
2181
    return True
2182

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

2186
    """
2187
    pass
2188

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

2192
    """
2193
    pass
2194

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

2198
    """
2199
    pass
2200

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

2204
    """
2205
    pass
2206

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

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

    
2213

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

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

    
2223

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

    
2228

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

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

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

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

    
2251

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

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

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

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

    
2273

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

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

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