Statistics
| Branch: | Tag: | Revision:

root / lib / bdev.py @ 8a69b3a8

History | View | Annotate | Download (75.8 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
    desired_stripes = params[constants.STRIPES]
419
    stripes = min(current_pvs, desired_stripes)
420
    if stripes < desired_stripes:
421
      logging.warning("Could not use %d stripes for VG %s, as only %d PVs are"
422
                      " available.", desired_stripes, vg_name, current_pvs)
423

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

    
443
  @staticmethod
444
  def _GetVolumeInfo(lvm_cmd, fields):
445
    """Returns LVM Volumen infos using lvm_cmd
446

447
    @param lvm_cmd: Should be one of "pvs", "vgs" or "lvs"
448
    @param fields: Fields to return
449
    @return: A list of dicts each with the parsed fields
450

451
    """
452
    if not fields:
453
      raise errors.ProgrammerError("No fields specified")
454

    
455
    sep = "|"
456
    cmd = [lvm_cmd, "--noheadings", "--nosuffix", "--units=m", "--unbuffered",
457
           "--separator=%s" % sep, "-o%s" % ",".join(fields)]
458

    
459
    result = utils.RunCmd(cmd)
460
    if result.failed:
461
      raise errors.CommandError("Can't get the volume information: %s - %s" %
462
                                (result.fail_reason, result.output))
463

    
464
    data = []
465
    for line in result.stdout.splitlines():
466
      splitted_fields = line.strip().split(sep)
467

    
468
      if len(fields) != len(splitted_fields):
469
        raise errors.CommandError("Can't parse %s output: line '%s'" %
470
                                  (lvm_cmd, line))
471

    
472
      data.append(splitted_fields)
473

    
474
    return data
475

    
476
  @classmethod
477
  def GetPVInfo(cls, vg_names, filter_allocatable=True):
478
    """Get the free space info for PVs in a volume group.
479

480
    @param vg_names: list of volume group names, if empty all will be returned
481
    @param filter_allocatable: whether to skip over unallocatable PVs
482

483
    @rtype: list
484
    @return: list of tuples (free_space, name) with free_space in mebibytes
485

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

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

    
504
    return data
505

    
506
  @classmethod
507
  def GetVGInfo(cls, vg_names, filter_readonly=True):
508
    """Get the free space info for specific VGs.
509

510
    @param vg_names: list of volume group names, if empty all will be returned
511
    @param filter_readonly: whether to skip over readonly VGs
512

513
    @rtype: list
514
    @return: list of tuples (free_space, total_size, name) with free_space in
515
             MiB
516

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

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

    
535
    return data
536

    
537
  @classmethod
538
  def _ValidateName(cls, name):
539
    """Validates that a given name is valid as VG or LV name.
540

541
    The list of valid characters and restricted names is taken out of
542
    the lvm(8) manpage, with the simplification that we enforce both
543
    VG and LV restrictions on the names.
544

545
    """
546
    if (not cls._VALID_NAME_RE.match(name) or
547
        name in cls._INVALID_NAMES or
548
        compat.any(substring in name for substring in cls._INVALID_SUBSTRINGS)):
549
      _ThrowError("Invalid LVM name '%s'", name)
550

    
551
  def Remove(self):
552
    """Remove this logical volume.
553

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

    
563
  def Rename(self, new_id):
564
    """Rename this logical volume.
565

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

    
580
  def Attach(self):
581
    """Attach to an existing LV.
582

583
    This method will try to see if an existing and active LV exists
584
    which matches our name. If so, its major/minor will be
585
    recorded.
586

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

    
613
    status, major, minor, pe_size, stripes = out
614
    if len(status) != 6:
615
      logging.error("lvs lv_attr is not 6 characters (%s)", status)
616
      return False
617

    
618
    try:
619
      major = int(major)
620
      minor = int(minor)
621
    except (TypeError, ValueError), err:
622
      logging.error("lvs major/minor cannot be parsed: %s", str(err))
623

    
624
    try:
625
      pe_size = int(float(pe_size))
626
    except (TypeError, ValueError), err:
627
      logging.error("Can't parse vg extent size: %s", err)
628
      return False
629

    
630
    try:
631
      stripes = int(stripes)
632
    except (TypeError, ValueError), err:
633
      logging.error("Can't parse the number of stripes: %s", err)
634
      return False
635

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

    
645
  def Assemble(self):
646
    """Assemble the device.
647

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

652
    """
653
    result = utils.RunCmd(["lvchange", "-ay", self.dev_path])
654
    if result.failed:
655
      _ThrowError("Can't activate lv %s: %s", self.dev_path, result.output)
656

    
657
  def Shutdown(self):
658
    """Shutdown the device.
659

660
    This is a no-op for the LV device type, as we don't deactivate the
661
    volumes on shutdown.
662

663
    """
664
    pass
665

    
666
  def GetSyncStatus(self):
667
    """Returns the sync status of the device.
668

669
    If this device is a mirroring device, this function returns the
670
    status of the mirror.
671

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

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

683
    The status was already read in Attach, so we just return it.
684

685
    @rtype: objects.BlockDevStatus
686

687
    """
688
    if self._degraded:
689
      ldisk_status = constants.LDS_FAULTY
690
    else:
691
      ldisk_status = constants.LDS_OKAY
692

    
693
    return objects.BlockDevStatus(dev_path=self.dev_path,
694
                                  major=self.major,
695
                                  minor=self.minor,
696
                                  sync_percent=None,
697
                                  estimated_time=None,
698
                                  is_degraded=self._degraded,
699
                                  ldisk_status=ldisk_status)
700

    
701
  def Open(self, force=False):
702
    """Make the device ready for I/O.
703

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

706
    """
707
    pass
708

    
709
  def Close(self):
710
    """Notifies that the device will no longer be used for I/O.
711

712
    This is a no-op for the LV device type.
713

714
    """
715
    pass
716

    
717
  def Snapshot(self, size):
718
    """Create a snapshot copy of an lvm block device.
719

720
    @returns: tuple (vg, lv)
721

722
    """
723
    snap_name = self._lv_name + ".snap"
724

    
725
    # remove existing snapshot if found
726
    snap = LogicalVolume((self._vg_name, snap_name), None, size, self.params)
727
    _IgnoreError(snap.Remove)
728

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

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

    
743
    return (self._vg_name, snap_name)
744

    
745
  def SetInfo(self, text):
746
    """Update metadata with info text.
747

748
    """
749
    BlockDev.SetInfo(self, text)
750

    
751
    # Replace invalid characters
752
    text = re.sub("^[^A-Za-z0-9_+.]", "_", text)
753
    text = re.sub("[^-A-Za-z0-9_+.]", "_", text)
754

    
755
    # Only up to 128 characters are allowed
756
    text = text[:128]
757

    
758
    result = utils.RunCmd(["lvchange", "--addtag", text,
759
                           self.dev_path])
760
    if result.failed:
761
      _ThrowError("Command: %s error: %s - %s", result.cmd, result.fail_reason,
762
                  result.output)
763

    
764
  def Grow(self, amount, dryrun):
765
    """Grow the logical volume.
766

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

    
788

    
789
class DRBD8Status(object):
790
  """A DRBD status representation class.
791

792
  Note that this doesn't support unconfigured devices (cs:Unconfigured).
793

794
  """
795
  UNCONF_RE = re.compile(r"\s*[0-9]+:\s*cs:Unconfigured$")
796
  LINE_RE = re.compile(r"\s*[0-9]+:\s*cs:(\S+)\s+(?:st|ro):([^/]+)/(\S+)"
797
                       "\s+ds:([^/]+)/(\S+)\s+.*$")
798
  SYNC_RE = re.compile(r"^.*\ssync'ed:\s*([0-9.]+)%.*"
799
                       # Due to a bug in drbd in the kernel, introduced in
800
                       # commit 4b0715f096 (still unfixed as of 2011-08-22)
801
                       "(?:\s|M)"
802
                       "finish: ([0-9]+):([0-9]+):([0-9]+)\s.*$")
803

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

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

    
841
  RO_PRIMARY = "Primary"
842
  RO_SECONDARY = "Secondary"
843
  RO_UNKNOWN = "Unknown"
844

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

    
860
    # end reading of data from the LINE_RE or UNCONF_RE
861

    
862
    self.is_standalone = self.cstatus == self.CS_STANDALONE
863
    self.is_wfconn = self.cstatus == self.CS_WFCONNECTION
864
    self.is_connected = self.cstatus == self.CS_CONNECTED
865
    self.is_primary = self.lrole == self.RO_PRIMARY
866
    self.is_secondary = self.lrole == self.RO_SECONDARY
867
    self.peer_primary = self.rrole == self.RO_PRIMARY
868
    self.peer_secondary = self.rrole == self.RO_SECONDARY
869
    self.both_primary = self.is_primary and self.peer_primary
870
    self.both_secondary = self.is_secondary and self.peer_secondary
871

    
872
    self.is_diskless = self.ldisk == self.DS_DISKLESS
873
    self.is_disk_uptodate = self.ldisk == self.DS_UPTODATE
874

    
875
    self.is_in_resync = self.cstatus in self.CSET_SYNC
876
    self.is_in_use = self.cstatus != self.CS_UNCONFIGURED
877

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

    
896

    
897
class BaseDRBD(BlockDev): # pylint: disable=W0223
898
  """Base DRBD class.
899

900
  This class contains a few bits of common functionality between the
901
  0.7 and 8.x versions of DRBD.
902

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

    
909
  _DRBD_MAJOR = 147
910
  _ST_UNCONFIGURED = "Unconfigured"
911
  _ST_WFCONNECTION = "WFConnection"
912
  _ST_CONNECTED = "Connected"
913

    
914
  _STATUS_FILE = "/proc/drbd"
915
  _USERMODE_HELPER_FILE = "/sys/module/drbd/parameters/usermode_helper"
916

    
917
  @staticmethod
918
  def _GetProcData(filename=_STATUS_FILE):
919
    """Return data from /proc/drbd.
920

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

    
934
  @classmethod
935
  def _MassageProcData(cls, data):
936
    """Transform the output of _GetProdData into a nicer form.
937

938
    @return: a dictionary of minor: joined lines from /proc/drbd
939
        for that minor
940

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

    
961
  @classmethod
962
  def _GetVersion(cls, proc_data):
963
    """Return the DRBD version.
964

965
    This will return a dict with keys:
966
      - k_major
967
      - k_minor
968
      - k_point
969
      - api
970
      - proto
971
      - proto2 (only on drbd > 8.2.X)
972

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

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

    
990
    return retval
991

    
992
  @staticmethod
993
  def GetUsermodeHelper(filename=_USERMODE_HELPER_FILE):
994
    """Returns DRBD usermode_helper currently set.
995

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

    
1009
  @staticmethod
1010
  def _DevPath(minor):
1011
    """Return the path to a drbd device for a given minor.
1012

1013
    """
1014
    return "/dev/drbd%d" % minor
1015

    
1016
  @classmethod
1017
  def GetUsedDevs(cls):
1018
    """Compute the list of used DRBD devices.
1019

1020
    """
1021
    data = cls._GetProcData()
1022

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

    
1034
    return used_devs
1035

    
1036
  def _SetFromMinor(self, minor):
1037
    """Set our parameters based on the given minor.
1038

1039
    This sets our minor variable and our dev_path.
1040

1041
    """
1042
    if minor is None:
1043
      self.minor = self.dev_path = None
1044
      self.attached = False
1045
    else:
1046
      self.minor = minor
1047
      self.dev_path = self._DevPath(minor)
1048
      self.attached = True
1049

    
1050
  @staticmethod
1051
  def _CheckMetaSize(meta_device):
1052
    """Check if the given meta device looks like a valid one.
1053

1054
    This currently only check the size, which must be around
1055
    128MiB.
1056

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

    
1078
  def Rename(self, new_id):
1079
    """Rename a device.
1080

1081
    This is not supported for drbd devices.
1082

1083
    """
1084
    raise errors.ProgrammerError("Can't rename a drbd device")
1085

    
1086

    
1087
class DRBD8(BaseDRBD):
1088
  """DRBD v8.x block device.
1089

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

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

1099
  """
1100
  _MAX_MINORS = 255
1101
  _PARSE_SHOW = None
1102

    
1103
  # timeout constants
1104
  _NET_RECONFIG_TIMEOUT = 60
1105

    
1106
  # command line options for barriers
1107
  _DISABLE_DISK_OPTION = "--no-disk-barrier"  # -a
1108
  _DISABLE_DRAIN_OPTION = "--no-disk-drain"   # -D
1109
  _DISABLE_FLUSH_OPTION = "--no-disk-flushes" # -i
1110
  _DISABLE_META_FLUSH_OPTION = "--no-md-flushes"  # -m
1111

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

    
1134
    if (self._lhost is not None and self._lhost == self._rhost and
1135
        self._lport == self._rport):
1136
      raise ValueError("Invalid configuration data, same local/remote %s" %
1137
                       (unique_id,))
1138
    self.Attach()
1139

    
1140
  @classmethod
1141
  def _InitMeta(cls, minor, dev_path):
1142
    """Initialize a meta device.
1143

1144
    This will not work if the given minor is in use.
1145

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

    
1158
    result = utils.RunCmd(["drbdmeta", "--force", cls._DevPath(minor),
1159
                           "v08", dev_path, "0", "create-md"])
1160
    if result.failed:
1161
      _ThrowError("Can't initialize meta device: %s", result.output)
1162

    
1163
  @classmethod
1164
  def _FindUnusedMinor(cls):
1165
    """Find an unused DRBD device.
1166

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

1170
    """
1171
    data = cls._GetProcData()
1172

    
1173
    highest = None
1174
    for line in data:
1175
      match = cls._UNUSED_LINE_RE.match(line)
1176
      if match:
1177
        return int(match.group(1))
1178
      match = cls._VALID_LINE_RE.match(line)
1179
      if match:
1180
        minor = int(match.group(1))
1181
        highest = max(highest, minor)
1182
    if highest is None: # there are no minors in use at all
1183
      return 0
1184
    if highest >= cls._MAX_MINORS:
1185
      logging.error("Error: no free drbd minors!")
1186
      raise errors.BlockDeviceError("Can't find a free DRBD minor")
1187
    return highest + 1
1188

    
1189
  @classmethod
1190
  def _GetShowParser(cls):
1191
    """Return a parser for `drbd show` output.
1192

1193
    This will either create or return an already-create parser for the
1194
    output of the command `drbd show`.
1195

1196
    """
1197
    if cls._PARSE_SHOW is not None:
1198
      return cls._PARSE_SHOW
1199

    
1200
    # pyparsing setup
1201
    lbrace = pyp.Literal("{").suppress()
1202
    rbrace = pyp.Literal("}").suppress()
1203
    lbracket = pyp.Literal("[").suppress()
1204
    rbracket = pyp.Literal("]").suppress()
1205
    semi = pyp.Literal(";").suppress()
1206
    colon = pyp.Literal(":").suppress()
1207
    # this also converts the value to an int
1208
    number = pyp.Word(pyp.nums).setParseAction(lambda s, l, t: int(t[0]))
1209

    
1210
    comment = pyp.Literal("#") + pyp.Optional(pyp.restOfLine)
1211
    defa = pyp.Literal("_is_default").suppress()
1212
    dbl_quote = pyp.Literal('"').suppress()
1213

    
1214
    keyword = pyp.Word(pyp.alphanums + '-')
1215

    
1216
    # value types
1217
    value = pyp.Word(pyp.alphanums + '_-/.:')
1218
    quoted = dbl_quote + pyp.CharsNotIn('"') + dbl_quote
1219
    ipv4_addr = (pyp.Optional(pyp.Literal("ipv4")).suppress() +
1220
                 pyp.Word(pyp.nums + ".") + colon + number)
1221
    ipv6_addr = (pyp.Optional(pyp.Literal("ipv6")).suppress() +
1222
                 pyp.Optional(lbracket) + pyp.Word(pyp.hexnums + ":") +
1223
                 pyp.Optional(rbracket) + colon + number)
1224
    # meta device, extended syntax
1225
    meta_value = ((value ^ quoted) + lbracket + number + rbracket)
1226
    # device name, extended syntax
1227
    device_value = pyp.Literal("minor").suppress() + number
1228

    
1229
    # a statement
1230
    stmt = (~rbrace + keyword + ~lbrace +
1231
            pyp.Optional(ipv4_addr ^ ipv6_addr ^ value ^ quoted ^ meta_value ^
1232
                         device_value) +
1233
            pyp.Optional(defa) + semi +
1234
            pyp.Optional(pyp.restOfLine).suppress())
1235

    
1236
    # an entire section
1237
    section_name = pyp.Word(pyp.alphas + "_")
1238
    section = section_name + lbrace + pyp.ZeroOrMore(pyp.Group(stmt)) + rbrace
1239

    
1240
    bnf = pyp.ZeroOrMore(pyp.Group(section ^ stmt))
1241
    bnf.ignore(comment)
1242

    
1243
    cls._PARSE_SHOW = bnf
1244

    
1245
    return bnf
1246

    
1247
  @classmethod
1248
  def _GetShowData(cls, minor):
1249
    """Return the `drbdsetup show` data for a minor.
1250

1251
    """
1252
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "show"])
1253
    if result.failed:
1254
      logging.error("Can't display the drbd config: %s - %s",
1255
                    result.fail_reason, result.output)
1256
      return None
1257
    return result.stdout
1258

    
1259
  @classmethod
1260
  def _GetDevInfo(cls, out):
1261
    """Parse details about a given DRBD minor.
1262

1263
    This return, if available, the local backing device (as a path)
1264
    and the local and remote (ip, port) information from a string
1265
    containing the output of the `drbdsetup show` command as returned
1266
    by _GetShowData.
1267

1268
    """
1269
    data = {}
1270
    if not out:
1271
      return data
1272

    
1273
    bnf = cls._GetShowParser()
1274
    # run pyparse
1275

    
1276
    try:
1277
      results = bnf.parseString(out)
1278
    except pyp.ParseException, err:
1279
      _ThrowError("Can't parse drbdsetup show output: %s", str(err))
1280

    
1281
    # and massage the results into our desired format
1282
    for section in results:
1283
      sname = section[0]
1284
      if sname == "_this_host":
1285
        for lst in section[1:]:
1286
          if lst[0] == "disk":
1287
            data["local_dev"] = lst[1]
1288
          elif lst[0] == "meta-disk":
1289
            data["meta_dev"] = lst[1]
1290
            data["meta_index"] = lst[2]
1291
          elif lst[0] == "address":
1292
            data["local_addr"] = tuple(lst[1:])
1293
      elif sname == "_remote_host":
1294
        for lst in section[1:]:
1295
          if lst[0] == "address":
1296
            data["remote_addr"] = tuple(lst[1:])
1297
    return data
1298

    
1299
  def _MatchesLocal(self, info):
1300
    """Test if our local config matches with an existing device.
1301

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

1307
    """
1308
    if self._children:
1309
      backend, meta = self._children
1310
    else:
1311
      backend = meta = None
1312

    
1313
    if backend is not None:
1314
      retval = ("local_dev" in info and info["local_dev"] == backend.dev_path)
1315
    else:
1316
      retval = ("local_dev" not in info)
1317

    
1318
    if meta is not None:
1319
      retval = retval and ("meta_dev" in info and
1320
                           info["meta_dev"] == meta.dev_path)
1321
      retval = retval and ("meta_index" in info and
1322
                           info["meta_index"] == 0)
1323
    else:
1324
      retval = retval and ("meta_dev" not in info and
1325
                           "meta_index" not in info)
1326
    return retval
1327

    
1328
  def _MatchesNet(self, info):
1329
    """Test if our network config matches with an existing device.
1330

1331
    The parameter should be as returned from `_GetDevInfo()`. This
1332
    method tests if our network configuration is the same as the one
1333
    in the info parameter, in effect testing if we look like the given
1334
    device.
1335

1336
    """
1337
    if (((self._lhost is None and not ("local_addr" in info)) and
1338
         (self._rhost is None and not ("remote_addr" in info)))):
1339
      return True
1340

    
1341
    if self._lhost is None:
1342
      return False
1343

    
1344
    if not ("local_addr" in info and
1345
            "remote_addr" in info):
1346
      return False
1347

    
1348
    retval = (info["local_addr"] == (self._lhost, self._lport))
1349
    retval = (retval and
1350
              info["remote_addr"] == (self._rhost, self._rport))
1351
    return retval
1352

    
1353
  def _AssembleLocal(self, minor, backend, meta, size):
1354
    """Configure the local part of a DRBD device.
1355

1356
    """
1357
    args = ["drbdsetup", self._DevPath(minor), "disk",
1358
            backend, meta, "0",
1359
            "-e", "detach",
1360
            "--create-device"]
1361
    if size:
1362
      args.extend(["-d", "%sm" % size])
1363

    
1364
    version = self._GetVersion(self._GetProcData())
1365
    vmaj = version["k_major"]
1366
    vmin = version["k_minor"]
1367
    vrel = version["k_point"]
1368

    
1369
    barrier_args = \
1370
      self._ComputeDiskBarrierArgs(vmaj, vmin, vrel,
1371
                                   self.params[constants.BARRIERS],
1372
                                   self.params[constants.NO_META_FLUSH])
1373
    args.extend(barrier_args)
1374

    
1375
    result = utils.RunCmd(args)
1376
    if result.failed:
1377
      _ThrowError("drbd%d: can't attach local disk: %s", minor, result.output)
1378

    
1379
  @classmethod
1380
  def _ComputeDiskBarrierArgs(cls, vmaj, vmin, vrel, disabled_barriers,
1381
      disable_meta_flush):
1382
    """Compute the DRBD command line parameters for disk barriers
1383

1384
    Returns a list of the disk barrier parameters as requested via the
1385
    disabled_barriers and disable_meta_flush arguments, and according to the
1386
    supported ones in the DRBD version vmaj.vmin.vrel
1387

1388
    If the desired option is unsupported, raises errors.BlockDeviceError.
1389

1390
    """
1391
    disabled_barriers_set = frozenset(disabled_barriers)
1392
    if not disabled_barriers_set in constants.DRBD_VALID_BARRIER_OPT:
1393
      raise errors.BlockDeviceError("%s is not a valid option set for DRBD"
1394
                                    " barriers" % disabled_barriers)
1395

    
1396
    args = []
1397

    
1398
    # The following code assumes DRBD 8.x, with x < 4 and x != 1 (DRBD 8.1.x
1399
    # does not exist)
1400
    if not vmaj == 8 and vmin in (0, 2, 3):
1401
      raise errors.BlockDeviceError("Unsupported DRBD version: %d.%d.%d" %
1402
                                    (vmaj, vmin, vrel))
1403

    
1404
    def _AppendOrRaise(option, min_version):
1405
      """Helper for DRBD options"""
1406
      if min_version is not None and vrel >= min_version:
1407
        args.append(option)
1408
      else:
1409
        raise errors.BlockDeviceError("Could not use the option %s as the"
1410
                                      " DRBD version %d.%d.%d does not support"
1411
                                      " it." % (option, vmaj, vmin, vrel))
1412

    
1413
    # the minimum version for each feature is encoded via pairs of (minor
1414
    # version -> x) where x is version in which support for the option was
1415
    # introduced.
1416
    meta_flush_supported = disk_flush_supported = {
1417
      0: 12,
1418
      2: 7,
1419
      3: 0,
1420
      }
1421

    
1422
    disk_drain_supported = {
1423
      2: 7,
1424
      3: 0,
1425
      }
1426

    
1427
    disk_barriers_supported = {
1428
      3: 0,
1429
      }
1430

    
1431
    # meta flushes
1432
    if disable_meta_flush:
1433
      _AppendOrRaise(cls._DISABLE_META_FLUSH_OPTION,
1434
                     meta_flush_supported.get(vmin, None))
1435

    
1436
    # disk flushes
1437
    if constants.DRBD_B_DISK_FLUSH in disabled_barriers_set:
1438
      _AppendOrRaise(cls._DISABLE_FLUSH_OPTION,
1439
                     disk_flush_supported.get(vmin, None))
1440

    
1441
    # disk drain
1442
    if constants.DRBD_B_DISK_DRAIN in disabled_barriers_set:
1443
      _AppendOrRaise(cls._DISABLE_DRAIN_OPTION,
1444
                     disk_drain_supported.get(vmin, None))
1445

    
1446
    # disk barriers
1447
    if constants.DRBD_B_DISK_BARRIERS in disabled_barriers_set:
1448
      _AppendOrRaise(cls._DISABLE_DISK_OPTION,
1449
                     disk_barriers_supported.get(vmin, None))
1450

    
1451
    return args
1452

    
1453
  def _AssembleNet(self, minor, net_info, protocol,
1454
                   dual_pri=False, hmac=None, secret=None):
1455
    """Configure the network part of the device.
1456

1457
    """
1458
    lhost, lport, rhost, rport = net_info
1459
    if None in net_info:
1460
      # we don't want network connection and actually want to make
1461
      # sure its shutdown
1462
      self._ShutdownNet(minor)
1463
      return
1464

    
1465
    # Workaround for a race condition. When DRBD is doing its dance to
1466
    # establish a connection with its peer, it also sends the
1467
    # synchronization speed over the wire. In some cases setting the
1468
    # sync speed only after setting up both sides can race with DRBD
1469
    # connecting, hence we set it here before telling DRBD anything
1470
    # about its peer.
1471
    sync_speed = self.params.get(constants.RESYNC_RATE)
1472
    self._SetMinorSyncSpeed(minor, sync_speed)
1473

    
1474
    if netutils.IP6Address.IsValid(lhost):
1475
      if not netutils.IP6Address.IsValid(rhost):
1476
        _ThrowError("drbd%d: can't connect ip %s to ip %s" %
1477
                    (minor, lhost, rhost))
1478
      family = "ipv6"
1479
    elif netutils.IP4Address.IsValid(lhost):
1480
      if not netutils.IP4Address.IsValid(rhost):
1481
        _ThrowError("drbd%d: can't connect ip %s to ip %s" %
1482
                    (minor, lhost, rhost))
1483
      family = "ipv4"
1484
    else:
1485
      _ThrowError("drbd%d: Invalid ip %s" % (minor, lhost))
1486

    
1487
    args = ["drbdsetup", self._DevPath(minor), "net",
1488
            "%s:%s:%s" % (family, lhost, lport),
1489
            "%s:%s:%s" % (family, rhost, rport), protocol,
1490
            "-A", "discard-zero-changes",
1491
            "-B", "consensus",
1492
            "--create-device",
1493
            ]
1494
    if dual_pri:
1495
      args.append("-m")
1496
    if hmac and secret:
1497
      args.extend(["-a", hmac, "-x", secret])
1498
    result = utils.RunCmd(args)
1499
    if result.failed:
1500
      _ThrowError("drbd%d: can't setup network: %s - %s",
1501
                  minor, result.fail_reason, result.output)
1502

    
1503
    def _CheckNetworkConfig():
1504
      info = self._GetDevInfo(self._GetShowData(minor))
1505
      if not "local_addr" in info or not "remote_addr" in info:
1506
        raise utils.RetryAgain()
1507

    
1508
      if (info["local_addr"] != (lhost, lport) or
1509
          info["remote_addr"] != (rhost, rport)):
1510
        raise utils.RetryAgain()
1511

    
1512
    try:
1513
      utils.Retry(_CheckNetworkConfig, 1.0, 10.0)
1514
    except utils.RetryTimeout:
1515
      _ThrowError("drbd%d: timeout while configuring network", minor)
1516

    
1517
  def AddChildren(self, devices):
1518
    """Add a disk to the DRBD device.
1519

1520
    """
1521
    if self.minor is None:
1522
      _ThrowError("drbd%d: can't attach to dbrd8 during AddChildren",
1523
                  self._aminor)
1524
    if len(devices) != 2:
1525
      _ThrowError("drbd%d: need two devices for AddChildren", self.minor)
1526
    info = self._GetDevInfo(self._GetShowData(self.minor))
1527
    if "local_dev" in info:
1528
      _ThrowError("drbd%d: already attached to a local disk", self.minor)
1529
    backend, meta = devices
1530
    if backend.dev_path is None or meta.dev_path is None:
1531
      _ThrowError("drbd%d: children not ready during AddChildren", self.minor)
1532
    backend.Open()
1533
    meta.Open()
1534
    self._CheckMetaSize(meta.dev_path)
1535
    self._InitMeta(self._FindUnusedMinor(), meta.dev_path)
1536

    
1537
    self._AssembleLocal(self.minor, backend.dev_path, meta.dev_path, self.size)
1538
    self._children = devices
1539

    
1540
  def RemoveChildren(self, devices):
1541
    """Detach the drbd device from local storage.
1542

1543
    """
1544
    if self.minor is None:
1545
      _ThrowError("drbd%d: can't attach to drbd8 during RemoveChildren",
1546
                  self._aminor)
1547
    # early return if we don't actually have backing storage
1548
    info = self._GetDevInfo(self._GetShowData(self.minor))
1549
    if "local_dev" not in info:
1550
      return
1551
    if len(self._children) != 2:
1552
      _ThrowError("drbd%d: we don't have two children: %s", self.minor,
1553
                  self._children)
1554
    if self._children.count(None) == 2: # we don't actually have children :)
1555
      logging.warning("drbd%d: requested detach while detached", self.minor)
1556
      return
1557
    if len(devices) != 2:
1558
      _ThrowError("drbd%d: we need two children in RemoveChildren", self.minor)
1559
    for child, dev in zip(self._children, devices):
1560
      if dev != child.dev_path:
1561
        _ThrowError("drbd%d: mismatch in local storage (%s != %s) in"
1562
                    " RemoveChildren", self.minor, dev, child.dev_path)
1563

    
1564
    self._ShutdownLocal(self.minor)
1565
    self._children = []
1566

    
1567
  @classmethod
1568
  def _SetMinorSyncSpeed(cls, minor, kbytes):
1569
    """Set the speed of the DRBD syncer.
1570

1571
    This is the low-level implementation.
1572

1573
    @type minor: int
1574
    @param minor: the drbd minor whose settings we change
1575
    @type kbytes: int
1576
    @param kbytes: the speed in kbytes/second
1577
    @rtype: boolean
1578
    @return: the success of the operation
1579

1580
    """
1581
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "syncer",
1582
                           "-r", "%d" % kbytes, "--create-device"])
1583
    if result.failed:
1584
      logging.error("Can't change syncer rate: %s - %s",
1585
                    result.fail_reason, result.output)
1586
    return not result.failed
1587

    
1588
  def SetSyncSpeed(self, kbytes):
1589
    """Set the speed of the DRBD syncer.
1590

1591
    @type kbytes: int
1592
    @param kbytes: the speed in kbytes/second
1593
    @rtype: boolean
1594
    @return: the success of the operation
1595

1596
    """
1597
    if self.minor is None:
1598
      logging.info("Not attached during SetSyncSpeed")
1599
      return False
1600
    children_result = super(DRBD8, self).SetSyncSpeed(kbytes)
1601
    return self._SetMinorSyncSpeed(self.minor, kbytes) and children_result
1602

    
1603
  def PauseResumeSync(self, pause):
1604
    """Pauses or resumes the sync of a DRBD device.
1605

1606
    @param pause: Wether to pause or resume
1607
    @return: the success of the operation
1608

1609
    """
1610
    if self.minor is None:
1611
      logging.info("Not attached during PauseSync")
1612
      return False
1613

    
1614
    children_result = super(DRBD8, self).PauseResumeSync(pause)
1615

    
1616
    if pause:
1617
      cmd = "pause-sync"
1618
    else:
1619
      cmd = "resume-sync"
1620

    
1621
    result = utils.RunCmd(["drbdsetup", self.dev_path, cmd])
1622
    if result.failed:
1623
      logging.error("Can't %s: %s - %s", cmd,
1624
                    result.fail_reason, result.output)
1625
    return not result.failed and children_result
1626

    
1627
  def GetProcStatus(self):
1628
    """Return device data from /proc.
1629

1630
    """
1631
    if self.minor is None:
1632
      _ThrowError("drbd%d: GetStats() called while not attached", self._aminor)
1633
    proc_info = self._MassageProcData(self._GetProcData())
1634
    if self.minor not in proc_info:
1635
      _ThrowError("drbd%d: can't find myself in /proc", self.minor)
1636
    return DRBD8Status(proc_info[self.minor])
1637

    
1638
  def GetSyncStatus(self):
1639
    """Returns the sync status of the device.
1640

1641

1642
    If sync_percent is None, it means all is ok
1643
    If estimated_time is None, it means we can't estimate
1644
    the time needed, otherwise it's the time left in seconds.
1645

1646

1647
    We set the is_degraded parameter to True on two conditions:
1648
    network not connected or local disk missing.
1649

1650
    We compute the ldisk parameter based on whether we have a local
1651
    disk or not.
1652

1653
    @rtype: objects.BlockDevStatus
1654

1655
    """
1656
    if self.minor is None and not self.Attach():
1657
      _ThrowError("drbd%d: can't Attach() in GetSyncStatus", self._aminor)
1658

    
1659
    stats = self.GetProcStatus()
1660
    is_degraded = not stats.is_connected or not stats.is_disk_uptodate
1661

    
1662
    if stats.is_disk_uptodate:
1663
      ldisk_status = constants.LDS_OKAY
1664
    elif stats.is_diskless:
1665
      ldisk_status = constants.LDS_FAULTY
1666
    else:
1667
      ldisk_status = constants.LDS_UNKNOWN
1668

    
1669
    return objects.BlockDevStatus(dev_path=self.dev_path,
1670
                                  major=self.major,
1671
                                  minor=self.minor,
1672
                                  sync_percent=stats.sync_percent,
1673
                                  estimated_time=stats.est_time,
1674
                                  is_degraded=is_degraded,
1675
                                  ldisk_status=ldisk_status)
1676

    
1677
  def Open(self, force=False):
1678
    """Make the local state primary.
1679

1680
    If the 'force' parameter is given, the '-o' option is passed to
1681
    drbdsetup. Since this is a potentially dangerous operation, the
1682
    force flag should be only given after creation, when it actually
1683
    is mandatory.
1684

1685
    """
1686
    if self.minor is None and not self.Attach():
1687
      logging.error("DRBD cannot attach to a device during open")
1688
      return False
1689
    cmd = ["drbdsetup", self.dev_path, "primary"]
1690
    if force:
1691
      cmd.append("-o")
1692
    result = utils.RunCmd(cmd)
1693
    if result.failed:
1694
      _ThrowError("drbd%d: can't make drbd device primary: %s", self.minor,
1695
                  result.output)
1696

    
1697
  def Close(self):
1698
    """Make the local state secondary.
1699

1700
    This will, of course, fail if the device is in use.
1701

1702
    """
1703
    if self.minor is None and not self.Attach():
1704
      _ThrowError("drbd%d: can't Attach() in Close()", self._aminor)
1705
    result = utils.RunCmd(["drbdsetup", self.dev_path, "secondary"])
1706
    if result.failed:
1707
      _ThrowError("drbd%d: can't switch drbd device to secondary: %s",
1708
                  self.minor, result.output)
1709

    
1710
  def DisconnectNet(self):
1711
    """Removes network configuration.
1712

1713
    This method shutdowns the network side of the device.
1714

1715
    The method will wait up to a hardcoded timeout for the device to
1716
    go into standalone after the 'disconnect' command before
1717
    re-configuring it, as sometimes it takes a while for the
1718
    disconnect to actually propagate and thus we might issue a 'net'
1719
    command while the device is still connected. If the device will
1720
    still be attached to the network and we time out, we raise an
1721
    exception.
1722

1723
    """
1724
    if self.minor is None:
1725
      _ThrowError("drbd%d: disk not attached in re-attach net", self._aminor)
1726

    
1727
    if None in (self._lhost, self._lport, self._rhost, self._rport):
1728
      _ThrowError("drbd%d: DRBD disk missing network info in"
1729
                  " DisconnectNet()", self.minor)
1730

    
1731
    class _DisconnectStatus:
1732
      def __init__(self, ever_disconnected):
1733
        self.ever_disconnected = ever_disconnected
1734

    
1735
    dstatus = _DisconnectStatus(_IgnoreError(self._ShutdownNet, self.minor))
1736

    
1737
    def _WaitForDisconnect():
1738
      if self.GetProcStatus().is_standalone:
1739
        return
1740

    
1741
      # retry the disconnect, it seems possible that due to a well-time
1742
      # disconnect on the peer, my disconnect command might be ignored and
1743
      # forgotten
1744
      dstatus.ever_disconnected = \
1745
        _IgnoreError(self._ShutdownNet, self.minor) or dstatus.ever_disconnected
1746

    
1747
      raise utils.RetryAgain()
1748

    
1749
    # Keep start time
1750
    start_time = time.time()
1751

    
1752
    try:
1753
      # Start delay at 100 milliseconds and grow up to 2 seconds
1754
      utils.Retry(_WaitForDisconnect, (0.1, 1.5, 2.0),
1755
                  self._NET_RECONFIG_TIMEOUT)
1756
    except utils.RetryTimeout:
1757
      if dstatus.ever_disconnected:
1758
        msg = ("drbd%d: device did not react to the"
1759
               " 'disconnect' command in a timely manner")
1760
      else:
1761
        msg = "drbd%d: can't shutdown network, even after multiple retries"
1762

    
1763
      _ThrowError(msg, self.minor)
1764

    
1765
    reconfig_time = time.time() - start_time
1766
    if reconfig_time > (self._NET_RECONFIG_TIMEOUT * 0.25):
1767
      logging.info("drbd%d: DisconnectNet: detach took %.3f seconds",
1768
                   self.minor, reconfig_time)
1769

    
1770
  def AttachNet(self, multimaster):
1771
    """Reconnects the network.
1772

1773
    This method connects the network side of the device with a
1774
    specified multi-master flag. The device needs to be 'Standalone'
1775
    but have valid network configuration data.
1776

1777
    Args:
1778
      - multimaster: init the network in dual-primary mode
1779

1780
    """
1781
    if self.minor is None:
1782
      _ThrowError("drbd%d: device not attached in AttachNet", self._aminor)
1783

    
1784
    if None in (self._lhost, self._lport, self._rhost, self._rport):
1785
      _ThrowError("drbd%d: missing network info in AttachNet()", self.minor)
1786

    
1787
    status = self.GetProcStatus()
1788

    
1789
    if not status.is_standalone:
1790
      _ThrowError("drbd%d: device is not standalone in AttachNet", self.minor)
1791

    
1792
    self._AssembleNet(self.minor,
1793
                      (self._lhost, self._lport, self._rhost, self._rport),
1794
                      constants.DRBD_NET_PROTOCOL, dual_pri=multimaster,
1795
                      hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1796

    
1797
  def Attach(self):
1798
    """Check if our minor is configured.
1799

1800
    This doesn't do any device configurations - it only checks if the
1801
    minor is in a state different from Unconfigured.
1802

1803
    Note that this function will not change the state of the system in
1804
    any way (except in case of side-effects caused by reading from
1805
    /proc).
1806

1807
    """
1808
    used_devs = self.GetUsedDevs()
1809
    if self._aminor in used_devs:
1810
      minor = self._aminor
1811
    else:
1812
      minor = None
1813

    
1814
    self._SetFromMinor(minor)
1815
    return minor is not None
1816

    
1817
  def Assemble(self):
1818
    """Assemble the drbd.
1819

1820
    Method:
1821
      - if we have a configured device, we try to ensure that it matches
1822
        our config
1823
      - if not, we create it from zero
1824
      - anyway, set the device parameters
1825

1826
    """
1827
    super(DRBD8, self).Assemble()
1828

    
1829
    self.Attach()
1830
    if self.minor is None:
1831
      # local device completely unconfigured
1832
      self._FastAssemble()
1833
    else:
1834
      # we have to recheck the local and network status and try to fix
1835
      # the device
1836
      self._SlowAssemble()
1837

    
1838
    sync_speed = self.params.get(constants.RESYNC_RATE)
1839
    self.SetSyncSpeed(sync_speed)
1840

    
1841
  def _SlowAssemble(self):
1842
    """Assembles the DRBD device from a (partially) configured device.
1843

1844
    In case of partially attached (local device matches but no network
1845
    setup), we perform the network attach. If successful, we re-test
1846
    the attach if can return success.
1847

1848
    """
1849
    # TODO: Rewrite to not use a for loop just because there is 'break'
1850
    # pylint: disable=W0631
1851
    net_data = (self._lhost, self._lport, self._rhost, self._rport)
1852
    for minor in (self._aminor,):
1853
      info = self._GetDevInfo(self._GetShowData(minor))
1854
      match_l = self._MatchesLocal(info)
1855
      match_r = self._MatchesNet(info)
1856

    
1857
      if match_l and match_r:
1858
        # everything matches
1859
        break
1860

    
1861
      if match_l and not match_r and "local_addr" not in info:
1862
        # disk matches, but not attached to network, attach and recheck
1863
        self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
1864
                          hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1865
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1866
          break
1867
        else:
1868
          _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
1869
                      " show' disagrees", minor)
1870

    
1871
      if match_r and "local_dev" not in info:
1872
        # no local disk, but network attached and it matches
1873
        self._AssembleLocal(minor, self._children[0].dev_path,
1874
                            self._children[1].dev_path, self.size)
1875
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1876
          break
1877
        else:
1878
          _ThrowError("drbd%d: disk attach successful, but 'drbdsetup"
1879
                      " show' disagrees", minor)
1880

    
1881
      # this case must be considered only if we actually have local
1882
      # storage, i.e. not in diskless mode, because all diskless
1883
      # devices are equal from the point of view of local
1884
      # configuration
1885
      if (match_l and "local_dev" in info and
1886
          not match_r and "local_addr" in info):
1887
        # strange case - the device network part points to somewhere
1888
        # else, even though its local storage is ours; as we own the
1889
        # drbd space, we try to disconnect from the remote peer and
1890
        # reconnect to our correct one
1891
        try:
1892
          self._ShutdownNet(minor)
1893
        except errors.BlockDeviceError, err:
1894
          _ThrowError("drbd%d: device has correct local storage, wrong"
1895
                      " remote peer and is unable to disconnect in order"
1896
                      " to attach to the correct peer: %s", minor, str(err))
1897
        # note: _AssembleNet also handles the case when we don't want
1898
        # local storage (i.e. one or more of the _[lr](host|port) is
1899
        # None)
1900
        self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
1901
                          hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1902
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1903
          break
1904
        else:
1905
          _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
1906
                      " show' disagrees", minor)
1907

    
1908
    else:
1909
      minor = None
1910

    
1911
    self._SetFromMinor(minor)
1912
    if minor is None:
1913
      _ThrowError("drbd%d: cannot activate, unknown or unhandled reason",
1914
                  self._aminor)
1915

    
1916
  def _FastAssemble(self):
1917
    """Assemble the drbd device from zero.
1918

1919
    This is run when in Assemble we detect our minor is unused.
1920

1921
    """
1922
    minor = self._aminor
1923
    if self._children and self._children[0] and self._children[1]:
1924
      self._AssembleLocal(minor, self._children[0].dev_path,
1925
                          self._children[1].dev_path, self.size)
1926
    if self._lhost and self._lport and self._rhost and self._rport:
1927
      self._AssembleNet(minor,
1928
                        (self._lhost, self._lport, self._rhost, self._rport),
1929
                        constants.DRBD_NET_PROTOCOL,
1930
                        hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1931
    self._SetFromMinor(minor)
1932

    
1933
  @classmethod
1934
  def _ShutdownLocal(cls, minor):
1935
    """Detach from the local device.
1936

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

1940
    """
1941
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "detach"])
1942
    if result.failed:
1943
      _ThrowError("drbd%d: can't detach local disk: %s", minor, result.output)
1944

    
1945
  @classmethod
1946
  def _ShutdownNet(cls, minor):
1947
    """Disconnect from the remote peer.
1948

1949
    This fails if we don't have a local device.
1950

1951
    """
1952
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disconnect"])
1953
    if result.failed:
1954
      _ThrowError("drbd%d: can't shutdown network: %s", minor, result.output)
1955

    
1956
  @classmethod
1957
  def _ShutdownAll(cls, minor):
1958
    """Deactivate the device.
1959

1960
    This will, of course, fail if the device is in use.
1961

1962
    """
1963
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "down"])
1964
    if result.failed:
1965
      _ThrowError("drbd%d: can't shutdown drbd device: %s",
1966
                  minor, result.output)
1967

    
1968
  def Shutdown(self):
1969
    """Shutdown the DRBD device.
1970

1971
    """
1972
    if self.minor is None and not self.Attach():
1973
      logging.info("drbd%d: not attached during Shutdown()", self._aminor)
1974
      return
1975
    minor = self.minor
1976
    self.minor = None
1977
    self.dev_path = None
1978
    self._ShutdownAll(minor)
1979

    
1980
  def Remove(self):
1981
    """Stub remove for DRBD devices.
1982

1983
    """
1984
    self.Shutdown()
1985

    
1986
  @classmethod
1987
  def Create(cls, unique_id, children, size, params):
1988
    """Create a new DRBD8 device.
1989

1990
    Since DRBD devices are not created per se, just assembled, this
1991
    function only initializes the metadata.
1992

1993
    """
1994
    if len(children) != 2:
1995
      raise errors.ProgrammerError("Invalid setup for the drbd device")
1996
    # check that the minor is unused
1997
    aminor = unique_id[4]
1998
    proc_info = cls._MassageProcData(cls._GetProcData())
1999
    if aminor in proc_info:
2000
      status = DRBD8Status(proc_info[aminor])
2001
      in_use = status.is_in_use
2002
    else:
2003
      in_use = False
2004
    if in_use:
2005
      _ThrowError("drbd%d: minor is already in use at Create() time", aminor)
2006
    meta = children[1]
2007
    meta.Assemble()
2008
    if not meta.Attach():
2009
      _ThrowError("drbd%d: can't attach to meta device '%s'",
2010
                  aminor, meta)
2011
    cls._CheckMetaSize(meta.dev_path)
2012
    cls._InitMeta(aminor, meta.dev_path)
2013
    return cls(unique_id, children, size, params)
2014

    
2015
  def Grow(self, amount, dryrun):
2016
    """Resize the DRBD device and its backing storage.
2017

2018
    """
2019
    if self.minor is None:
2020
      _ThrowError("drbd%d: Grow called while not attached", self._aminor)
2021
    if len(self._children) != 2 or None in self._children:
2022
      _ThrowError("drbd%d: cannot grow diskless device", self.minor)
2023
    self._children[0].Grow(amount, dryrun)
2024
    if dryrun:
2025
      # DRBD does not support dry-run mode, so we'll return here
2026
      return
2027
    result = utils.RunCmd(["drbdsetup", self.dev_path, "resize", "-s",
2028
                           "%dm" % (self.size + amount)])
2029
    if result.failed:
2030
      _ThrowError("drbd%d: resize failed: %s", self.minor, result.output)
2031

    
2032

    
2033
class FileStorage(BlockDev):
2034
  """File device.
2035

2036
  This class represents the a file storage backend device.
2037

2038
  The unique_id for the file device is a (file_driver, file_path) tuple.
2039

2040
  """
2041
  def __init__(self, unique_id, children, size, params):
2042
    """Initalizes a file device backend.
2043

2044
    """
2045
    if children:
2046
      raise errors.BlockDeviceError("Invalid setup for file device")
2047
    super(FileStorage, self).__init__(unique_id, children, size, params)
2048
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
2049
      raise ValueError("Invalid configuration data %s" % str(unique_id))
2050
    self.driver = unique_id[0]
2051
    self.dev_path = unique_id[1]
2052
    self.Attach()
2053

    
2054
  def Assemble(self):
2055
    """Assemble the device.
2056

2057
    Checks whether the file device exists, raises BlockDeviceError otherwise.
2058

2059
    """
2060
    if not os.path.exists(self.dev_path):
2061
      _ThrowError("File device '%s' does not exist" % self.dev_path)
2062

    
2063
  def Shutdown(self):
2064
    """Shutdown the device.
2065

2066
    This is a no-op for the file type, as we don't deactivate
2067
    the file on shutdown.
2068

2069
    """
2070
    pass
2071

    
2072
  def Open(self, force=False):
2073
    """Make the device ready for I/O.
2074

2075
    This is a no-op for the file type.
2076

2077
    """
2078
    pass
2079

    
2080
  def Close(self):
2081
    """Notifies that the device will no longer be used for I/O.
2082

2083
    This is a no-op for the file type.
2084

2085
    """
2086
    pass
2087

    
2088
  def Remove(self):
2089
    """Remove the file backing the block device.
2090

2091
    @rtype: boolean
2092
    @return: True if the removal was successful
2093

2094
    """
2095
    try:
2096
      os.remove(self.dev_path)
2097
    except OSError, err:
2098
      if err.errno != errno.ENOENT:
2099
        _ThrowError("Can't remove file '%s': %s", self.dev_path, err)
2100

    
2101
  def Rename(self, new_id):
2102
    """Renames the file.
2103

2104
    """
2105
    # TODO: implement rename for file-based storage
2106
    _ThrowError("Rename is not supported for file-based storage")
2107

    
2108
  def Grow(self, amount, dryrun):
2109
    """Grow the file
2110

2111
    @param amount: the amount (in mebibytes) to grow with
2112

2113
    """
2114
    # Check that the file exists
2115
    self.Assemble()
2116
    current_size = self.GetActualSize()
2117
    new_size = current_size + amount * 1024 * 1024
2118
    assert new_size > current_size, "Cannot Grow with a negative amount"
2119
    # We can't really simulate the growth
2120
    if dryrun:
2121
      return
2122
    try:
2123
      f = open(self.dev_path, "a+")
2124
      f.truncate(new_size)
2125
      f.close()
2126
    except EnvironmentError, err:
2127
      _ThrowError("Error in file growth: %", str(err))
2128

    
2129
  def Attach(self):
2130
    """Attach to an existing file.
2131

2132
    Check if this file already exists.
2133

2134
    @rtype: boolean
2135
    @return: True if file exists
2136

2137
    """
2138
    self.attached = os.path.exists(self.dev_path)
2139
    return self.attached
2140

    
2141
  def GetActualSize(self):
2142
    """Return the actual disk size.
2143

2144
    @note: the device needs to be active when this is called
2145

2146
    """
2147
    assert self.attached, "BlockDevice not attached in GetActualSize()"
2148
    try:
2149
      st = os.stat(self.dev_path)
2150
      return st.st_size
2151
    except OSError, err:
2152
      _ThrowError("Can't stat %s: %s", self.dev_path, err)
2153

    
2154
  @classmethod
2155
  def Create(cls, unique_id, children, size, params):
2156
    """Create a new file.
2157

2158
    @param size: the size of file in MiB
2159

2160
    @rtype: L{bdev.FileStorage}
2161
    @return: an instance of FileStorage
2162

2163
    """
2164
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
2165
      raise ValueError("Invalid configuration data %s" % str(unique_id))
2166
    dev_path = unique_id[1]
2167
    try:
2168
      fd = os.open(dev_path, os.O_RDWR | os.O_CREAT | os.O_EXCL)
2169
      f = os.fdopen(fd, "w")
2170
      f.truncate(size * 1024 * 1024)
2171
      f.close()
2172
    except EnvironmentError, err:
2173
      if err.errno == errno.EEXIST:
2174
        _ThrowError("File already existing: %s", dev_path)
2175
      _ThrowError("Error in file creation: %", str(err))
2176

    
2177
    return FileStorage(unique_id, children, size, params)
2178

    
2179

    
2180
class PersistentBlockDevice(BlockDev):
2181
  """A block device with persistent node
2182

2183
  May be either directly attached, or exposed through DM (e.g. dm-multipath).
2184
  udev helpers are probably required to give persistent, human-friendly
2185
  names.
2186

2187
  For the time being, pathnames are required to lie under /dev.
2188

2189
  """
2190
  def __init__(self, unique_id, children, size, params):
2191
    """Attaches to a static block device.
2192

2193
    The unique_id is a path under /dev.
2194

2195
    """
2196
    super(PersistentBlockDevice, self).__init__(unique_id, children, size,
2197
                                                params)
2198
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
2199
      raise ValueError("Invalid configuration data %s" % str(unique_id))
2200
    self.dev_path = unique_id[1]
2201
    if not os.path.realpath(self.dev_path).startswith("/dev/"):
2202
      raise ValueError("Full path '%s' lies outside /dev" %
2203
                              os.path.realpath(self.dev_path))
2204
    # TODO: this is just a safety guard checking that we only deal with devices
2205
    # we know how to handle. In the future this will be integrated with
2206
    # external storage backends and possible values will probably be collected
2207
    # from the cluster configuration.
2208
    if unique_id[0] != constants.BLOCKDEV_DRIVER_MANUAL:
2209
      raise ValueError("Got persistent block device of invalid type: %s" %
2210
                       unique_id[0])
2211

    
2212
    self.major = self.minor = None
2213
    self.Attach()
2214

    
2215
  @classmethod
2216
  def Create(cls, unique_id, children, size, params):
2217
    """Create a new device
2218

2219
    This is a noop, we only return a PersistentBlockDevice instance
2220

2221
    """
2222
    return PersistentBlockDevice(unique_id, children, 0, params)
2223

    
2224
  def Remove(self):
2225
    """Remove a device
2226

2227
    This is a noop
2228

2229
    """
2230
    pass
2231

    
2232
  def Rename(self, new_id):
2233
    """Rename this device.
2234

2235
    """
2236
    _ThrowError("Rename is not supported for PersistentBlockDev storage")
2237

    
2238
  def Attach(self):
2239
    """Attach to an existing block device.
2240

2241

2242
    """
2243
    self.attached = False
2244
    try:
2245
      st = os.stat(self.dev_path)
2246
    except OSError, err:
2247
      logging.error("Error stat()'ing %s: %s", self.dev_path, str(err))
2248
      return False
2249

    
2250
    if not stat.S_ISBLK(st.st_mode):
2251
      logging.error("%s is not a block device", self.dev_path)
2252
      return False
2253

    
2254
    self.major = os.major(st.st_rdev)
2255
    self.minor = os.minor(st.st_rdev)
2256
    self.attached = True
2257

    
2258
    return True
2259

    
2260
  def Assemble(self):
2261
    """Assemble the device.
2262

2263
    """
2264
    pass
2265

    
2266
  def Shutdown(self):
2267
    """Shutdown the device.
2268

2269
    """
2270
    pass
2271

    
2272
  def Open(self, force=False):
2273
    """Make the device ready for I/O.
2274

2275
    """
2276
    pass
2277

    
2278
  def Close(self):
2279
    """Notifies that the device will no longer be used for I/O.
2280

2281
    """
2282
    pass
2283

    
2284
  def Grow(self, amount, dryrun):
2285
    """Grow the logical volume.
2286

2287
    """
2288
    _ThrowError("Grow is not supported for PersistentBlockDev storage")
2289

    
2290

    
2291
DEV_MAP = {
2292
  constants.LD_LV: LogicalVolume,
2293
  constants.LD_DRBD8: DRBD8,
2294
  constants.LD_BLOCKDEV: PersistentBlockDevice,
2295
  }
2296

    
2297
if constants.ENABLE_FILE_STORAGE or constants.ENABLE_SHARED_FILE_STORAGE:
2298
  DEV_MAP[constants.LD_FILE] = FileStorage
2299

    
2300

    
2301
def _VerifyDiskType(dev_type):
2302
  if dev_type not in DEV_MAP:
2303
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
2304

    
2305

    
2306
def FindDevice(disk, children):
2307
  """Search for an existing, assembled device.
2308

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

2312
  @type disk: L{objects.Disk}
2313
  @param disk: the disk object to find
2314
  @type children: list of L{bdev.BlockDev}
2315
  @param children: the list of block devices that are children of the device
2316
                  represented by the disk parameter
2317

2318
  """
2319
  _VerifyDiskType(disk.dev_type)
2320
  dev_params = objects.FillDict(constants.DISK_LD_DEFAULTS[disk.dev_type],
2321
                                disk.params)
2322
  device = DEV_MAP[disk.dev_type](disk.physical_id, children, disk.size,
2323
                                  dev_params)
2324
  if not device.attached:
2325
    return None
2326
  return device
2327

    
2328

    
2329
def Assemble(disk, children):
2330
  """Try to attach or assemble an existing device.
2331

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

2335
  @type disk: L{objects.Disk}
2336
  @param disk: the disk object to assemble
2337
  @type children: list of L{bdev.BlockDev}
2338
  @param children: the list of block devices that are children of the device
2339
                  represented by the disk parameter
2340

2341
  """
2342
  _VerifyDiskType(disk.dev_type)
2343
  dev_params = objects.FillDict(constants.DISK_LD_DEFAULTS[disk.dev_type],
2344
                                disk.params)
2345
  device = DEV_MAP[disk.dev_type](disk.physical_id, children, disk.size,
2346
                                  dev_params)
2347
  device.Assemble()
2348
  return device
2349

    
2350

    
2351
def Create(disk, children):
2352
  """Create a device.
2353

2354
  @type disk: L{objects.Disk}
2355
  @param disk: the disk object to create
2356
  @type children: list of L{bdev.BlockDev}
2357
  @param children: the list of block devices that are children of the device
2358
                  represented by the disk parameter
2359

2360
  """
2361
  _VerifyDiskType(disk.dev_type)
2362
  dev_params = objects.FillDict(constants.DISK_LD_DEFAULTS[disk.dev_type],
2363
                                disk.params)
2364
  device = DEV_MAP[disk.dev_type].Create(disk.physical_id, children, disk.size,
2365
                                         dev_params)
2366
  return device