Statistics
| Branch: | Tag: | Revision:

root / lib / bdev.py @ 728489a3

History | View | Annotate | Download (63.2 kB)

1
#
2
#
3

    
4
# Copyright (C) 2006, 2007 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 pyparsing as pyp
28
import os
29
import logging
30

    
31
from ganeti import utils
32
from ganeti import errors
33
from ganeti import constants
34
from ganeti import objects
35

    
36

    
37
# Size of reads in _CanReadDevice
38
_DEVICE_READ_SIZE = 128 * 1024
39

    
40

    
41
def _IgnoreError(fn, *args, **kwargs):
42
  """Executes the given function, ignoring BlockDeviceErrors.
43

44
  This is used in order to simplify the execution of cleanup or
45
  rollback functions.
46

47
  @rtype: boolean
48
  @return: True when fn didn't raise an exception, False otherwise
49

50
  """
51
  try:
52
    fn(*args, **kwargs)
53
    return True
54
  except errors.BlockDeviceError, err:
55
    logging.warning("Caught BlockDeviceError but ignoring: %s", str(err))
56
    return False
57

    
58

    
59
def _ThrowError(msg, *args):
60
  """Log an error to the node daemon and the raise an exception.
61

62
  @type msg: string
63
  @param msg: the text of the exception
64
  @raise errors.BlockDeviceError
65

66
  """
67
  if args:
68
    msg = msg % args
69
  logging.error(msg)
70
  raise errors.BlockDeviceError(msg)
71

    
72

    
73
def _CanReadDevice(path):
74
  """Check if we can read from the given device.
75

76
  This tries to read the first 128k of the device.
77

78
  """
79
  try:
80
    utils.ReadFile(path, size=_DEVICE_READ_SIZE)
81
    return True
82
  except EnvironmentError:
83
    logging.warning("Can't read from device %s", path, exc_info=True)
84
    return False
85

    
86

    
87
class BlockDev(object):
88
  """Block device abstract class.
89

90
  A block device can be in the following states:
91
    - not existing on the system, and by `Create()` it goes into:
92
    - existing but not setup/not active, and by `Assemble()` goes into:
93
    - active read-write and by `Open()` it goes into
94
    - online (=used, or ready for use)
95

96
  A device can also be online but read-only, however we are not using
97
  the readonly state (LV has it, if needed in the future) and we are
98
  usually looking at this like at a stack, so it's easier to
99
  conceptualise the transition from not-existing to online and back
100
  like a linear one.
101

102
  The many different states of the device are due to the fact that we
103
  need to cover many device types:
104
    - logical volumes are created, lvchange -a y $lv, and used
105
    - drbd devices are attached to a local disk/remote peer and made primary
106

107
  A block device is identified by three items:
108
    - the /dev path of the device (dynamic)
109
    - a unique ID of the device (static)
110
    - it's major/minor pair (dynamic)
111

112
  Not all devices implement both the first two as distinct items. LVM
113
  logical volumes have their unique ID (the pair volume group, logical
114
  volume name) in a 1-to-1 relation to the dev path. For DRBD devices,
115
  the /dev path is again dynamic and the unique id is the pair (host1,
116
  dev1), (host2, dev2).
117

118
  You can get to a device in two ways:
119
    - creating the (real) device, which returns you
120
      an attached instance (lvcreate)
121
    - attaching of a python instance to an existing (real) device
122

123
  The second point, the attachement to a device, is different
124
  depending on whether the device is assembled or not. At init() time,
125
  we search for a device with the same unique_id as us. If found,
126
  good. It also means that the device is already assembled. If not,
127
  after assembly we'll have our correct major/minor.
128

129
  """
130
  def __init__(self, unique_id, children, size):
131
    self._children = children
132
    self.dev_path = None
133
    self.unique_id = unique_id
134
    self.major = None
135
    self.minor = None
136
    self.attached = False
137
    self.size = size
138

    
139
  def Assemble(self):
140
    """Assemble the device from its components.
141

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

150
    """
151
    pass
152

    
153
  def Attach(self):
154
    """Find a device which matches our config and attach to it.
155

156
    """
157
    raise NotImplementedError
158

    
159
  def Close(self):
160
    """Notifies that the device will no longer be used for I/O.
161

162
    """
163
    raise NotImplementedError
164

    
165
  @classmethod
166
  def Create(cls, unique_id, children, size):
167
    """Create the device.
168

169
    If the device cannot be created, it will return None
170
    instead. Error messages go to the logging system.
171

172
    Note that for some devices, the unique_id is used, and for other,
173
    the children. The idea is that these two, taken together, are
174
    enough for both creation and assembly (later).
175

176
    """
177
    raise NotImplementedError
178

    
179
  def Remove(self):
180
    """Remove this device.
181

182
    This makes sense only for some of the device types: LV and file
183
    storage. Also note that if the device can't attach, the removal
184
    can't be completed.
185

186
    """
187
    raise NotImplementedError
188

    
189
  def Rename(self, new_id):
190
    """Rename this device.
191

192
    This may or may not make sense for a given device type.
193

194
    """
195
    raise NotImplementedError
196

    
197
  def Open(self, force=False):
198
    """Make the device ready for use.
199

200
    This makes the device ready for I/O. For now, just the DRBD
201
    devices need this.
202

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

206
    """
207
    raise NotImplementedError
208

    
209
  def Shutdown(self):
210
    """Shut down the device, freeing its children.
211

212
    This undoes the `Assemble()` work, except for the child
213
    assembling; as such, the children on the device are still
214
    assembled after this call.
215

216
    """
217
    raise NotImplementedError
218

    
219
  def SetSyncSpeed(self, speed):
220
    """Adjust the sync speed of the mirror.
221

222
    In case this is not a mirroring device, this is no-op.
223

224
    """
225
    result = True
226
    if self._children:
227
      for child in self._children:
228
        result = result and child.SetSyncSpeed(speed)
229
    return result
230

    
231
  def GetSyncStatus(self):
232
    """Returns the sync status of the device.
233

234
    If this device is a mirroring device, this function returns the
235
    status of the mirror.
236

237
    If sync_percent is None, it means the device is not syncing.
238

239
    If estimated_time is None, it means we can't estimate
240
    the time needed, otherwise it's the time left in seconds.
241

242
    If is_degraded is True, it means the device is missing
243
    redundancy. This is usually a sign that something went wrong in
244
    the device setup, if sync_percent is None.
245

246
    The ldisk parameter represents the degradation of the local
247
    data. This is only valid for some devices, the rest will always
248
    return False (not degraded).
249

250
    @rtype: objects.BlockDevStatus
251

252
    """
253
    return objects.BlockDevStatus(dev_path=self.dev_path,
254
                                  major=self.major,
255
                                  minor=self.minor,
256
                                  sync_percent=None,
257
                                  estimated_time=None,
258
                                  is_degraded=False,
259
                                  ldisk_status=constants.LDS_OKAY)
260

    
261
  def CombinedSyncStatus(self):
262
    """Calculate the mirror status recursively for our children.
263

264
    The return value is the same as for `GetSyncStatus()` except the
265
    minimum percent and maximum time are calculated across our
266
    children.
267

268
    @rtype: objects.BlockDevStatus
269

270
    """
271
    status = self.GetSyncStatus()
272

    
273
    min_percent = status.sync_percent
274
    max_time = status.estimated_time
275
    is_degraded = status.is_degraded
276
    ldisk_status = status.ldisk_status
277

    
278
    if self._children:
279
      for child in self._children:
280
        child_status = child.GetSyncStatus()
281

    
282
        if min_percent is None:
283
          min_percent = child_status.sync_percent
284
        elif child_status.sync_percent is not None:
285
          min_percent = min(min_percent, child_status.sync_percent)
286

    
287
        if max_time is None:
288
          max_time = child_status.estimated_time
289
        elif child_status.estimated_time is not None:
290
          max_time = max(max_time, child_status.estimated_time)
291

    
292
        is_degraded = is_degraded or child_status.is_degraded
293

    
294
        if ldisk_status is None:
295
          ldisk_status = child_status.ldisk_status
296
        elif child_status.ldisk_status is not None:
297
          ldisk_status = max(ldisk_status, child_status.ldisk_status)
298

    
299
    return objects.BlockDevStatus(dev_path=self.dev_path,
300
                                  major=self.major,
301
                                  minor=self.minor,
302
                                  sync_percent=min_percent,
303
                                  estimated_time=max_time,
304
                                  is_degraded=is_degraded,
305
                                  ldisk_status=ldisk_status)
306

    
307

    
308
  def SetInfo(self, text):
309
    """Update metadata with info text.
310

311
    Only supported for some device types.
312

313
    """
314
    for child in self._children:
315
      child.SetInfo(text)
316

    
317
  def Grow(self, amount):
318
    """Grow the block device.
319

320
    @param amount: the amount (in mebibytes) to grow with
321

322
    """
323
    raise NotImplementedError
324

    
325
  def GetActualSize(self):
326
    """Return the actual disk size.
327

328
    @note: the device needs to be active when this is called
329

330
    """
331
    assert self.attached, "BlockDevice not attached in GetActualSize()"
332
    result = utils.RunCmd(["blockdev", "--getsize64", self.dev_path])
333
    if result.failed:
334
      _ThrowError("blockdev failed (%s): %s",
335
                  result.fail_reason, result.output)
336
    try:
337
      sz = int(result.output.strip())
338
    except (ValueError, TypeError), err:
339
      _ThrowError("Failed to parse blockdev output: %s", str(err))
340
    return sz
341

    
342
  def __repr__(self):
343
    return ("<%s: unique_id: %s, children: %s, %s:%s, %s>" %
344
            (self.__class__, self.unique_id, self._children,
345
             self.major, self.minor, self.dev_path))
346

    
347

    
348
class LogicalVolume(BlockDev):
349
  """Logical Volume block device.
350

351
  """
352
  _VALID_NAME_RE = re.compile("^[a-zA-Z0-9+_.-]*$")
353
  _INVALID_NAMES = frozenset([".", "..", "snapshot", "pvmove"])
354
  _INVALID_SUBSTRINGS = frozenset(["_mlog", "_mimage"])
355

    
356
  def __init__(self, unique_id, children, size):
357
    """Attaches to a LV device.
358

359
    The unique_id is a tuple (vg_name, lv_name)
360

361
    """
362
    super(LogicalVolume, self).__init__(unique_id, children, size)
363
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
364
      raise ValueError("Invalid configuration data %s" % str(unique_id))
365
    self._vg_name, self._lv_name = unique_id
366
    self._ValidateName(self._vg_name)
367
    self._ValidateName(self._lv_name)
368
    self.dev_path = utils.PathJoin("/dev", self._vg_name, self._lv_name)
369
    self._degraded = True
370
    self.major = self.minor = self.pe_size = self.stripe_count = None
371
    self.Attach()
372

    
373
  @classmethod
374
  def Create(cls, unique_id, children, size):
375
    """Create a new logical volume.
376

377
    """
378
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
379
      raise errors.ProgrammerError("Invalid configuration data %s" %
380
                                   str(unique_id))
381
    vg_name, lv_name = unique_id
382
    cls._ValidateName(vg_name)
383
    cls._ValidateName(lv_name)
384
    pvs_info = cls.GetPVInfo([vg_name])
385
    if not pvs_info:
386
      _ThrowError("Can't compute PV info for vg %s", vg_name)
387
    pvs_info.sort()
388
    pvs_info.reverse()
389

    
390
    pvlist = [ pv[1] for pv in pvs_info ]
391
    if utils.any(pvlist, lambda v: ":" in v):
392
      _ThrowError("Some of your PVs have the invalid character ':' in their"
393
                  " name, this is not supported - please filter them out"
394
                  " in lvm.conf using either 'filter' or 'preferred_names'")
395
    free_size = sum([ pv[0] for pv in pvs_info ])
396
    current_pvs = len(pvlist)
397
    stripes = min(current_pvs, constants.LVM_STRIPECOUNT)
398

    
399
    # The size constraint should have been checked from the master before
400
    # calling the create function.
401
    if free_size < size:
402
      _ThrowError("Not enough free space: required %s,"
403
                  " available %s", size, free_size)
404
    cmd = ["lvcreate", "-L%dm" % size, "-n%s" % lv_name]
405
    # If the free space is not well distributed, we won't be able to
406
    # create an optimally-striped volume; in that case, we want to try
407
    # with N, N-1, ..., 2, and finally 1 (non-stripped) number of
408
    # stripes
409
    for stripes_arg in range(stripes, 0, -1):
410
      result = utils.RunCmd(cmd + ["-i%d" % stripes_arg] + [vg_name] + pvlist)
411
      if not result.failed:
412
        break
413
    if result.failed:
414
      _ThrowError("LV create failed (%s): %s",
415
                  result.fail_reason, result.output)
416
    return LogicalVolume(unique_id, children, size)
417

    
418
  @staticmethod
419
  def GetPVInfo(vg_names, filter_allocatable=True):
420
    """Get the free space info for PVs in a volume group.
421

422
    @param vg_names: list of volume group names, if empty all will be returned
423
    @param filter_allocatable: whether to skip over unallocatable PVs
424

425
    @rtype: list
426
    @return: list of tuples (free_space, name) with free_space in mebibytes
427

428
    """
429
    sep = "|"
430
    command = ["pvs", "--noheadings", "--nosuffix", "--units=m",
431
               "-opv_name,vg_name,pv_free,pv_attr", "--unbuffered",
432
               "--separator=%s" % sep ]
433
    result = utils.RunCmd(command)
434
    if result.failed:
435
      logging.error("Can't get the PV information: %s - %s",
436
                    result.fail_reason, result.output)
437
      return None
438
    data = []
439
    for line in result.stdout.splitlines():
440
      fields = line.strip().split(sep)
441
      if len(fields) != 4:
442
        logging.error("Can't parse pvs output: line '%s'", line)
443
        return None
444
      # (possibly) skip over pvs which are not allocatable
445
      if filter_allocatable and fields[3][0] != 'a':
446
        continue
447
      # (possibly) skip over pvs which are not in the right volume group(s)
448
      if vg_names and fields[1] not in vg_names:
449
        continue
450
      data.append((float(fields[2]), fields[0], fields[1]))
451

    
452
    return data
453

    
454
  @classmethod
455
  def _ValidateName(cls, name):
456
    """Validates that a given name is valid as VG or LV name.
457

458
    The list of valid characters and restricted names is taken out of
459
    the lvm(8) manpage, with the simplification that we enforce both
460
    VG and LV restrictions on the names.
461

462
    """
463
    if (not cls._VALID_NAME_RE.match(name) or
464
        name in cls._INVALID_NAMES or
465
        utils.any(cls._INVALID_SUBSTRINGS, lambda x: x in name)):
466
      _ThrowError("Invalid LVM name '%s'", name)
467

    
468
  def Remove(self):
469
    """Remove this logical volume.
470

471
    """
472
    if not self.minor and not self.Attach():
473
      # the LV does not exist
474
      return
475
    result = utils.RunCmd(["lvremove", "-f", "%s/%s" %
476
                           (self._vg_name, self._lv_name)])
477
    if result.failed:
478
      _ThrowError("Can't lvremove: %s - %s", result.fail_reason, result.output)
479

    
480
  def Rename(self, new_id):
481
    """Rename this logical volume.
482

483
    """
484
    if not isinstance(new_id, (tuple, list)) or len(new_id) != 2:
485
      raise errors.ProgrammerError("Invalid new logical id '%s'" % new_id)
486
    new_vg, new_name = new_id
487
    if new_vg != self._vg_name:
488
      raise errors.ProgrammerError("Can't move a logical volume across"
489
                                   " volume groups (from %s to to %s)" %
490
                                   (self._vg_name, new_vg))
491
    result = utils.RunCmd(["lvrename", new_vg, self._lv_name, new_name])
492
    if result.failed:
493
      _ThrowError("Failed to rename the logical volume: %s", result.output)
494
    self._lv_name = new_name
495
    self.dev_path = utils.PathJoin("/dev", self._vg_name, self._lv_name)
496

    
497
  def Attach(self):
498
    """Attach to an existing LV.
499

500
    This method will try to see if an existing and active LV exists
501
    which matches our name. If so, its major/minor will be
502
    recorded.
503

504
    """
505
    self.attached = False
506
    result = utils.RunCmd(["lvs", "--noheadings", "--separator=,",
507
                           "--units=m", "--nosuffix",
508
                           "-olv_attr,lv_kernel_major,lv_kernel_minor,"
509
                           "vg_extent_size,stripes", self.dev_path])
510
    if result.failed:
511
      logging.error("Can't find LV %s: %s, %s",
512
                    self.dev_path, result.fail_reason, result.output)
513
      return False
514
    # the output can (and will) have multiple lines for multi-segment
515
    # LVs, as the 'stripes' parameter is a segment one, so we take
516
    # only the last entry, which is the one we're interested in; note
517
    # that with LVM2 anyway the 'stripes' value must be constant
518
    # across segments, so this is a no-op actually
519
    out = result.stdout.splitlines()
520
    if not out: # totally empty result? splitlines() returns at least
521
                # one line for any non-empty string
522
      logging.error("Can't parse LVS output, no lines? Got '%s'", str(out))
523
      return False
524
    out = out[-1].strip().rstrip(',')
525
    out = out.split(",")
526
    if len(out) != 5:
527
      logging.error("Can't parse LVS output, len(%s) != 5", str(out))
528
      return False
529

    
530
    status, major, minor, pe_size, stripes = out
531
    if len(status) != 6:
532
      logging.error("lvs lv_attr is not 6 characters (%s)", status)
533
      return False
534

    
535
    try:
536
      major = int(major)
537
      minor = int(minor)
538
    except (TypeError, ValueError), err:
539
      logging.error("lvs major/minor cannot be parsed: %s", str(err))
540

    
541
    try:
542
      pe_size = int(float(pe_size))
543
    except (TypeError, ValueError), err:
544
      logging.error("Can't parse vg extent size: %s", err)
545
      return False
546

    
547
    try:
548
      stripes = int(stripes)
549
    except (TypeError, ValueError), err:
550
      logging.error("Can't parse the number of stripes: %s", err)
551
      return False
552

    
553
    self.major = major
554
    self.minor = minor
555
    self.pe_size = pe_size
556
    self.stripe_count = stripes
557
    self._degraded = status[0] == 'v' # virtual volume, i.e. doesn't backing
558
                                      # storage
559
    self.attached = True
560
    return True
561

    
562
  def Assemble(self):
563
    """Assemble the device.
564

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

569
    """
570
    result = utils.RunCmd(["lvchange", "-ay", self.dev_path])
571
    if result.failed:
572
      _ThrowError("Can't activate lv %s: %s", self.dev_path, result.output)
573

    
574
  def Shutdown(self):
575
    """Shutdown the device.
576

577
    This is a no-op for the LV device type, as we don't deactivate the
578
    volumes on shutdown.
579

580
    """
581
    pass
582

    
583
  def GetSyncStatus(self):
584
    """Returns the sync status of the device.
585

586
    If this device is a mirroring device, this function returns the
587
    status of the mirror.
588

589
    For logical volumes, sync_percent and estimated_time are always
590
    None (no recovery in progress, as we don't handle the mirrored LV
591
    case). The is_degraded parameter is the inverse of the ldisk
592
    parameter.
593

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

600
    The status was already read in Attach, so we just return it.
601

602
    @rtype: objects.BlockDevStatus
603

604
    """
605
    if self._degraded:
606
      ldisk_status = constants.LDS_FAULTY
607
    else:
608
      ldisk_status = constants.LDS_OKAY
609

    
610
    return objects.BlockDevStatus(dev_path=self.dev_path,
611
                                  major=self.major,
612
                                  minor=self.minor,
613
                                  sync_percent=None,
614
                                  estimated_time=None,
615
                                  is_degraded=self._degraded,
616
                                  ldisk_status=ldisk_status)
617

    
618
  def Open(self, force=False):
619
    """Make the device ready for I/O.
620

621
    This is a no-op for the LV device type.
622

623
    """
624
    pass
625

    
626
  def Close(self):
627
    """Notifies that the device will no longer be used for I/O.
628

629
    This is a no-op for the LV device type.
630

631
    """
632
    pass
633

    
634
  def Snapshot(self, size):
635
    """Create a snapshot copy of an lvm block device.
636

637
    """
638
    snap_name = self._lv_name + ".snap"
639

    
640
    # remove existing snapshot if found
641
    snap = LogicalVolume((self._vg_name, snap_name), None, size)
642
    _IgnoreError(snap.Remove)
643

    
644
    pvs_info = self.GetPVInfo([self._vg_name])
645
    if not pvs_info:
646
      _ThrowError("Can't compute PV info for vg %s", self._vg_name)
647
    pvs_info.sort()
648
    pvs_info.reverse()
649
    free_size, _, _ = pvs_info[0]
650
    if free_size < size:
651
      _ThrowError("Not enough free space: required %s,"
652
                  " available %s", size, free_size)
653

    
654
    result = utils.RunCmd(["lvcreate", "-L%dm" % size, "-s",
655
                           "-n%s" % snap_name, self.dev_path])
656
    if result.failed:
657
      _ThrowError("command: %s error: %s - %s",
658
                  result.cmd, result.fail_reason, result.output)
659

    
660
    return snap_name
661

    
662
  def SetInfo(self, text):
663
    """Update metadata with info text.
664

665
    """
666
    BlockDev.SetInfo(self, text)
667

    
668
    # Replace invalid characters
669
    text = re.sub('^[^A-Za-z0-9_+.]', '_', text)
670
    text = re.sub('[^-A-Za-z0-9_+.]', '_', text)
671

    
672
    # Only up to 128 characters are allowed
673
    text = text[:128]
674

    
675
    result = utils.RunCmd(["lvchange", "--addtag", text,
676
                           self.dev_path])
677
    if result.failed:
678
      _ThrowError("Command: %s error: %s - %s", result.cmd, result.fail_reason,
679
                  result.output)
680

    
681
  def Grow(self, amount):
682
    """Grow the logical volume.
683

684
    """
685
    if self.pe_size is None or self.stripe_count is None:
686
      if not self.Attach():
687
        _ThrowError("Can't attach to LV during Grow()")
688
    full_stripe_size = self.pe_size * self.stripe_count
689
    rest = amount % full_stripe_size
690
    if rest != 0:
691
      amount += full_stripe_size - rest
692
    # we try multiple algorithms since the 'best' ones might not have
693
    # space available in the right place, but later ones might (since
694
    # they have less constraints); also note that only recent LVM
695
    # supports 'cling'
696
    for alloc_policy in "contiguous", "cling", "normal":
697
      result = utils.RunCmd(["lvextend", "--alloc", alloc_policy,
698
                             "-L", "+%dm" % amount, self.dev_path])
699
      if not result.failed:
700
        return
701
    _ThrowError("Can't grow LV %s: %s", self.dev_path, result.output)
702

    
703

    
704
class DRBD8Status(object):
705
  """A DRBD status representation class.
706

707
  Note that this doesn't support unconfigured devices (cs:Unconfigured).
708

709
  """
710
  UNCONF_RE = re.compile(r"\s*[0-9]+:\s*cs:Unconfigured$")
711
  LINE_RE = re.compile(r"\s*[0-9]+:\s*cs:(\S+)\s+(?:st|ro):([^/]+)/(\S+)"
712
                       "\s+ds:([^/]+)/(\S+)\s+.*$")
713
  SYNC_RE = re.compile(r"^.*\ssync'ed:\s*([0-9.]+)%.*"
714
                       "\sfinish: ([0-9]+):([0-9]+):([0-9]+)\s.*$")
715

    
716
  CS_UNCONFIGURED = "Unconfigured"
717
  CS_STANDALONE = "StandAlone"
718
  CS_WFCONNECTION = "WFConnection"
719
  CS_WFREPORTPARAMS = "WFReportParams"
720
  CS_CONNECTED = "Connected"
721
  CS_STARTINGSYNCS = "StartingSyncS"
722
  CS_STARTINGSYNCT = "StartingSyncT"
723
  CS_WFBITMAPS = "WFBitMapS"
724
  CS_WFBITMAPT = "WFBitMapT"
725
  CS_WFSYNCUUID = "WFSyncUUID"
726
  CS_SYNCSOURCE = "SyncSource"
727
  CS_SYNCTARGET = "SyncTarget"
728
  CS_PAUSEDSYNCS = "PausedSyncS"
729
  CS_PAUSEDSYNCT = "PausedSyncT"
730
  CSET_SYNC = frozenset([
731
    CS_WFREPORTPARAMS,
732
    CS_STARTINGSYNCS,
733
    CS_STARTINGSYNCT,
734
    CS_WFBITMAPS,
735
    CS_WFBITMAPT,
736
    CS_WFSYNCUUID,
737
    CS_SYNCSOURCE,
738
    CS_SYNCTARGET,
739
    CS_PAUSEDSYNCS,
740
    CS_PAUSEDSYNCT,
741
    ])
742

    
743
  DS_DISKLESS = "Diskless"
744
  DS_ATTACHING = "Attaching" # transient state
745
  DS_FAILED = "Failed" # transient state, next: diskless
746
  DS_NEGOTIATING = "Negotiating" # transient state
747
  DS_INCONSISTENT = "Inconsistent" # while syncing or after creation
748
  DS_OUTDATED = "Outdated"
749
  DS_DUNKNOWN = "DUnknown" # shown for peer disk when not connected
750
  DS_CONSISTENT = "Consistent"
751
  DS_UPTODATE = "UpToDate" # normal state
752

    
753
  RO_PRIMARY = "Primary"
754
  RO_SECONDARY = "Secondary"
755
  RO_UNKNOWN = "Unknown"
756

    
757
  def __init__(self, procline):
758
    u = self.UNCONF_RE.match(procline)
759
    if u:
760
      self.cstatus = self.CS_UNCONFIGURED
761
      self.lrole = self.rrole = self.ldisk = self.rdisk = None
762
    else:
763
      m = self.LINE_RE.match(procline)
764
      if not m:
765
        raise errors.BlockDeviceError("Can't parse input data '%s'" % procline)
766
      self.cstatus = m.group(1)
767
      self.lrole = m.group(2)
768
      self.rrole = m.group(3)
769
      self.ldisk = m.group(4)
770
      self.rdisk = m.group(5)
771

    
772
    # end reading of data from the LINE_RE or UNCONF_RE
773

    
774
    self.is_standalone = self.cstatus == self.CS_STANDALONE
775
    self.is_wfconn = self.cstatus == self.CS_WFCONNECTION
776
    self.is_connected = self.cstatus == self.CS_CONNECTED
777
    self.is_primary = self.lrole == self.RO_PRIMARY
778
    self.is_secondary = self.lrole == self.RO_SECONDARY
779
    self.peer_primary = self.rrole == self.RO_PRIMARY
780
    self.peer_secondary = self.rrole == self.RO_SECONDARY
781
    self.both_primary = self.is_primary and self.peer_primary
782
    self.both_secondary = self.is_secondary and self.peer_secondary
783

    
784
    self.is_diskless = self.ldisk == self.DS_DISKLESS
785
    self.is_disk_uptodate = self.ldisk == self.DS_UPTODATE
786

    
787
    self.is_in_resync = self.cstatus in self.CSET_SYNC
788
    self.is_in_use = self.cstatus != self.CS_UNCONFIGURED
789

    
790
    m = self.SYNC_RE.match(procline)
791
    if m:
792
      self.sync_percent = float(m.group(1))
793
      hours = int(m.group(2))
794
      minutes = int(m.group(3))
795
      seconds = int(m.group(4))
796
      self.est_time = hours * 3600 + minutes * 60 + seconds
797
    else:
798
      # we have (in this if branch) no percent information, but if
799
      # we're resyncing we need to 'fake' a sync percent information,
800
      # as this is how cmdlib determines if it makes sense to wait for
801
      # resyncing or not
802
      if self.is_in_resync:
803
        self.sync_percent = 0
804
      else:
805
        self.sync_percent = None
806
      self.est_time = None
807

    
808

    
809
class BaseDRBD(BlockDev): # pylint: disable-msg=W0223
810
  """Base DRBD class.
811

812
  This class contains a few bits of common functionality between the
813
  0.7 and 8.x versions of DRBD.
814

815
  """
816
  _VERSION_RE = re.compile(r"^version: (\d+)\.(\d+)\.(\d+)"
817
                           r" \(api:(\d+)/proto:(\d+)(?:-(\d+))?\)")
818
  _VALID_LINE_RE = re.compile("^ *([0-9]+): cs:([^ ]+).*$")
819
  _UNUSED_LINE_RE = re.compile("^ *([0-9]+): cs:Unconfigured$")
820

    
821
  _DRBD_MAJOR = 147
822
  _ST_UNCONFIGURED = "Unconfigured"
823
  _ST_WFCONNECTION = "WFConnection"
824
  _ST_CONNECTED = "Connected"
825

    
826
  _STATUS_FILE = "/proc/drbd"
827

    
828
  @staticmethod
829
  def _GetProcData(filename=_STATUS_FILE):
830
    """Return data from /proc/drbd.
831

832
    """
833
    try:
834
      data = utils.ReadFile(filename).splitlines()
835
    except EnvironmentError, err:
836
      if err.errno == errno.ENOENT:
837
        _ThrowError("The file %s cannot be opened, check if the module"
838
                    " is loaded (%s)", filename, str(err))
839
      else:
840
        _ThrowError("Can't read the DRBD proc file %s: %s", filename, str(err))
841
    if not data:
842
      _ThrowError("Can't read any data from %s", filename)
843
    return data
844

    
845
  @classmethod
846
  def _MassageProcData(cls, data):
847
    """Transform the output of _GetProdData into a nicer form.
848

849
    @return: a dictionary of minor: joined lines from /proc/drbd
850
        for that minor
851

852
    """
853
    results = {}
854
    old_minor = old_line = None
855
    for line in data:
856
      if not line: # completely empty lines, as can be returned by drbd8.0+
857
        continue
858
      lresult = cls._VALID_LINE_RE.match(line)
859
      if lresult is not None:
860
        if old_minor is not None:
861
          results[old_minor] = old_line
862
        old_minor = int(lresult.group(1))
863
        old_line = line
864
      else:
865
        if old_minor is not None:
866
          old_line += " " + line.strip()
867
    # add last line
868
    if old_minor is not None:
869
      results[old_minor] = old_line
870
    return results
871

    
872
  @classmethod
873
  def _GetVersion(cls):
874
    """Return the DRBD version.
875

876
    This will return a dict with keys:
877
      - k_major
878
      - k_minor
879
      - k_point
880
      - api
881
      - proto
882
      - proto2 (only on drbd > 8.2.X)
883

884
    """
885
    proc_data = cls._GetProcData()
886
    first_line = proc_data[0].strip()
887
    version = cls._VERSION_RE.match(first_line)
888
    if not version:
889
      raise errors.BlockDeviceError("Can't parse DRBD version from '%s'" %
890
                                    first_line)
891

    
892
    values = version.groups()
893
    retval = {'k_major': int(values[0]),
894
              'k_minor': int(values[1]),
895
              'k_point': int(values[2]),
896
              'api': int(values[3]),
897
              'proto': int(values[4]),
898
             }
899
    if values[5] is not None:
900
      retval['proto2'] = values[5]
901

    
902
    return retval
903

    
904
  @staticmethod
905
  def _DevPath(minor):
906
    """Return the path to a drbd device for a given minor.
907

908
    """
909
    return "/dev/drbd%d" % minor
910

    
911
  @classmethod
912
  def GetUsedDevs(cls):
913
    """Compute the list of used DRBD devices.
914

915
    """
916
    data = cls._GetProcData()
917

    
918
    used_devs = {}
919
    for line in data:
920
      match = cls._VALID_LINE_RE.match(line)
921
      if not match:
922
        continue
923
      minor = int(match.group(1))
924
      state = match.group(2)
925
      if state == cls._ST_UNCONFIGURED:
926
        continue
927
      used_devs[minor] = state, line
928

    
929
    return used_devs
930

    
931
  def _SetFromMinor(self, minor):
932
    """Set our parameters based on the given minor.
933

934
    This sets our minor variable and our dev_path.
935

936
    """
937
    if minor is None:
938
      self.minor = self.dev_path = None
939
      self.attached = False
940
    else:
941
      self.minor = minor
942
      self.dev_path = self._DevPath(minor)
943
      self.attached = True
944

    
945
  @staticmethod
946
  def _CheckMetaSize(meta_device):
947
    """Check if the given meta device looks like a valid one.
948

949
    This currently only check the size, which must be around
950
    128MiB.
951

952
    """
953
    result = utils.RunCmd(["blockdev", "--getsize", meta_device])
954
    if result.failed:
955
      _ThrowError("Failed to get device size: %s - %s",
956
                  result.fail_reason, result.output)
957
    try:
958
      sectors = int(result.stdout)
959
    except (TypeError, ValueError):
960
      _ThrowError("Invalid output from blockdev: '%s'", result.stdout)
961
    bytes = sectors * 512
962
    if bytes < 128 * 1024 * 1024: # less than 128MiB
963
      _ThrowError("Meta device too small (%.2fMib)", (bytes / 1024 / 1024))
964
    # the maximum *valid* size of the meta device when living on top
965
    # of LVM is hard to compute: it depends on the number of stripes
966
    # and the PE size; e.g. a 2-stripe, 64MB PE will result in a 128MB
967
    # (normal size), but an eight-stripe 128MB PE will result in a 1GB
968
    # size meta device; as such, we restrict it to 1GB (a little bit
969
    # too generous, but making assumptions about PE size is hard)
970
    if bytes > 1024 * 1024 * 1024:
971
      _ThrowError("Meta device too big (%.2fMiB)", (bytes / 1024 / 1024))
972

    
973
  def Rename(self, new_id):
974
    """Rename a device.
975

976
    This is not supported for drbd devices.
977

978
    """
979
    raise errors.ProgrammerError("Can't rename a drbd device")
980

    
981

    
982
class DRBD8(BaseDRBD):
983
  """DRBD v8.x block device.
984

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

989
  The unique_id for the drbd device is the (local_ip, local_port,
990
  remote_ip, remote_port) tuple, and it must have two children: the
991
  data device and the meta_device. The meta device is checked for
992
  valid size and is zeroed on create.
993

994
  """
995
  _MAX_MINORS = 255
996
  _PARSE_SHOW = None
997

    
998
  # timeout constants
999
  _NET_RECONFIG_TIMEOUT = 60
1000

    
1001
  def __init__(self, unique_id, children, size):
1002
    if children and children.count(None) > 0:
1003
      children = []
1004
    if len(children) not in (0, 2):
1005
      raise ValueError("Invalid configuration data %s" % str(children))
1006
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 6:
1007
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1008
    (self._lhost, self._lport,
1009
     self._rhost, self._rport,
1010
     self._aminor, self._secret) = unique_id
1011
    if children:
1012
      if not _CanReadDevice(children[1].dev_path):
1013
        logging.info("drbd%s: Ignoring unreadable meta device", self._aminor)
1014
        children = []
1015
    super(DRBD8, self).__init__(unique_id, children, size)
1016
    self.major = self._DRBD_MAJOR
1017
    version = self._GetVersion()
1018
    if version['k_major'] != 8 :
1019
      _ThrowError("Mismatch in DRBD kernel version and requested ganeti"
1020
                  " usage: kernel is %s.%s, ganeti wants 8.x",
1021
                  version['k_major'], version['k_minor'])
1022

    
1023
    if (self._lhost is not None and self._lhost == self._rhost and
1024
        self._lport == self._rport):
1025
      raise ValueError("Invalid configuration data, same local/remote %s" %
1026
                       (unique_id,))
1027
    self.Attach()
1028

    
1029
  @classmethod
1030
  def _InitMeta(cls, minor, dev_path):
1031
    """Initialize a meta device.
1032

1033
    This will not work if the given minor is in use.
1034

1035
    """
1036
    result = utils.RunCmd(["drbdmeta", "--force", cls._DevPath(minor),
1037
                           "v08", dev_path, "0", "create-md"])
1038
    if result.failed:
1039
      _ThrowError("Can't initialize meta device: %s", result.output)
1040

    
1041
  @classmethod
1042
  def _FindUnusedMinor(cls):
1043
    """Find an unused DRBD device.
1044

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

1048
    """
1049
    data = cls._GetProcData()
1050

    
1051
    highest = None
1052
    for line in data:
1053
      match = cls._UNUSED_LINE_RE.match(line)
1054
      if match:
1055
        return int(match.group(1))
1056
      match = cls._VALID_LINE_RE.match(line)
1057
      if match:
1058
        minor = int(match.group(1))
1059
        highest = max(highest, minor)
1060
    if highest is None: # there are no minors in use at all
1061
      return 0
1062
    if highest >= cls._MAX_MINORS:
1063
      logging.error("Error: no free drbd minors!")
1064
      raise errors.BlockDeviceError("Can't find a free DRBD minor")
1065
    return highest + 1
1066

    
1067
  @classmethod
1068
  def _GetShowParser(cls):
1069
    """Return a parser for `drbd show` output.
1070

1071
    This will either create or return an already-create parser for the
1072
    output of the command `drbd show`.
1073

1074
    """
1075
    if cls._PARSE_SHOW is not None:
1076
      return cls._PARSE_SHOW
1077

    
1078
    # pyparsing setup
1079
    lbrace = pyp.Literal("{").suppress()
1080
    rbrace = pyp.Literal("}").suppress()
1081
    semi = pyp.Literal(";").suppress()
1082
    # this also converts the value to an int
1083
    number = pyp.Word(pyp.nums).setParseAction(lambda s, l, t: int(t[0]))
1084

    
1085
    comment = pyp.Literal ("#") + pyp.Optional(pyp.restOfLine)
1086
    defa = pyp.Literal("_is_default").suppress()
1087
    dbl_quote = pyp.Literal('"').suppress()
1088

    
1089
    keyword = pyp.Word(pyp.alphanums + '-')
1090

    
1091
    # value types
1092
    value = pyp.Word(pyp.alphanums + '_-/.:')
1093
    quoted = dbl_quote + pyp.CharsNotIn('"') + dbl_quote
1094
    addr_type = (pyp.Optional(pyp.Literal("ipv4")).suppress() +
1095
                 pyp.Optional(pyp.Literal("ipv6")).suppress())
1096
    addr_port = (addr_type + pyp.Word(pyp.nums + '.') +
1097
                 pyp.Literal(':').suppress() + number)
1098
    # meta device, extended syntax
1099
    meta_value = ((value ^ quoted) + pyp.Literal('[').suppress() +
1100
                  number + pyp.Word(']').suppress())
1101
    # device name, extended syntax
1102
    device_value = pyp.Literal("minor").suppress() + number
1103

    
1104
    # a statement
1105
    stmt = (~rbrace + keyword + ~lbrace +
1106
            pyp.Optional(addr_port ^ value ^ quoted ^ meta_value ^
1107
                         device_value) +
1108
            pyp.Optional(defa) + semi +
1109
            pyp.Optional(pyp.restOfLine).suppress())
1110

    
1111
    # an entire section
1112
    section_name = pyp.Word(pyp.alphas + '_')
1113
    section = section_name + lbrace + pyp.ZeroOrMore(pyp.Group(stmt)) + rbrace
1114

    
1115
    bnf = pyp.ZeroOrMore(pyp.Group(section ^ stmt))
1116
    bnf.ignore(comment)
1117

    
1118
    cls._PARSE_SHOW = bnf
1119

    
1120
    return bnf
1121

    
1122
  @classmethod
1123
  def _GetShowData(cls, minor):
1124
    """Return the `drbdsetup show` data for a minor.
1125

1126
    """
1127
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "show"])
1128
    if result.failed:
1129
      logging.error("Can't display the drbd config: %s - %s",
1130
                    result.fail_reason, result.output)
1131
      return None
1132
    return result.stdout
1133

    
1134
  @classmethod
1135
  def _GetDevInfo(cls, out):
1136
    """Parse details about a given DRBD minor.
1137

1138
    This return, if available, the local backing device (as a path)
1139
    and the local and remote (ip, port) information from a string
1140
    containing the output of the `drbdsetup show` command as returned
1141
    by _GetShowData.
1142

1143
    """
1144
    data = {}
1145
    if not out:
1146
      return data
1147

    
1148
    bnf = cls._GetShowParser()
1149
    # run pyparse
1150

    
1151
    try:
1152
      results = bnf.parseString(out)
1153
    except pyp.ParseException, err:
1154
      _ThrowError("Can't parse drbdsetup show output: %s", str(err))
1155

    
1156
    # and massage the results into our desired format
1157
    for section in results:
1158
      sname = section[0]
1159
      if sname == "_this_host":
1160
        for lst in section[1:]:
1161
          if lst[0] == "disk":
1162
            data["local_dev"] = lst[1]
1163
          elif lst[0] == "meta-disk":
1164
            data["meta_dev"] = lst[1]
1165
            data["meta_index"] = lst[2]
1166
          elif lst[0] == "address":
1167
            data["local_addr"] = tuple(lst[1:])
1168
      elif sname == "_remote_host":
1169
        for lst in section[1:]:
1170
          if lst[0] == "address":
1171
            data["remote_addr"] = tuple(lst[1:])
1172
    return data
1173

    
1174
  def _MatchesLocal(self, info):
1175
    """Test if our local config matches with an existing device.
1176

1177
    The parameter should be as returned from `_GetDevInfo()`. This
1178
    method tests if our local backing device is the same as the one in
1179
    the info parameter, in effect testing if we look like the given
1180
    device.
1181

1182
    """
1183
    if self._children:
1184
      backend, meta = self._children
1185
    else:
1186
      backend = meta = None
1187

    
1188
    if backend is not None:
1189
      retval = ("local_dev" in info and info["local_dev"] == backend.dev_path)
1190
    else:
1191
      retval = ("local_dev" not in info)
1192

    
1193
    if meta is not None:
1194
      retval = retval and ("meta_dev" in info and
1195
                           info["meta_dev"] == meta.dev_path)
1196
      retval = retval and ("meta_index" in info and
1197
                           info["meta_index"] == 0)
1198
    else:
1199
      retval = retval and ("meta_dev" not in info and
1200
                           "meta_index" not in info)
1201
    return retval
1202

    
1203
  def _MatchesNet(self, info):
1204
    """Test if our network config matches with an existing device.
1205

1206
    The parameter should be as returned from `_GetDevInfo()`. This
1207
    method tests if our network configuration is the same as the one
1208
    in the info parameter, in effect testing if we look like the given
1209
    device.
1210

1211
    """
1212
    if (((self._lhost is None and not ("local_addr" in info)) and
1213
         (self._rhost is None and not ("remote_addr" in info)))):
1214
      return True
1215

    
1216
    if self._lhost is None:
1217
      return False
1218

    
1219
    if not ("local_addr" in info and
1220
            "remote_addr" in info):
1221
      return False
1222

    
1223
    retval = (info["local_addr"] == (self._lhost, self._lport))
1224
    retval = (retval and
1225
              info["remote_addr"] == (self._rhost, self._rport))
1226
    return retval
1227

    
1228
  @classmethod
1229
  def _AssembleLocal(cls, minor, backend, meta, size):
1230
    """Configure the local part of a DRBD device.
1231

1232
    """
1233
    args = ["drbdsetup", cls._DevPath(minor), "disk",
1234
            backend, meta, "0",
1235
            "-e", "detach",
1236
            "--create-device"]
1237
    if size:
1238
      args.extend(["-d", "%sm" % size])
1239
    if not constants.DRBD_BARRIERS: # disable barriers, if configured so
1240
      version = cls._GetVersion()
1241
      # various DRBD versions support different disk barrier options;
1242
      # what we aim here is to revert back to the 'drain' method of
1243
      # disk flushes and to disable metadata barriers, in effect going
1244
      # back to pre-8.0.7 behaviour
1245
      vmaj = version['k_major']
1246
      vmin = version['k_minor']
1247
      vrel = version['k_point']
1248
      assert vmaj == 8
1249
      if vmin == 0: # 8.0.x
1250
        if vrel >= 12:
1251
          args.extend(['-i', '-m'])
1252
      elif vmin == 2: # 8.2.x
1253
        if vrel >= 7:
1254
          args.extend(['-i', '-m'])
1255
      elif vmaj >= 3: # 8.3.x or newer
1256
        args.extend(['-i', '-a', 'm'])
1257
    result = utils.RunCmd(args)
1258
    if result.failed:
1259
      _ThrowError("drbd%d: can't attach local disk: %s", minor, result.output)
1260

    
1261
  @classmethod
1262
  def _AssembleNet(cls, minor, net_info, protocol,
1263
                   dual_pri=False, hmac=None, secret=None):
1264
    """Configure the network part of the device.
1265

1266
    """
1267
    lhost, lport, rhost, rport = net_info
1268
    if None in net_info:
1269
      # we don't want network connection and actually want to make
1270
      # sure its shutdown
1271
      cls._ShutdownNet(minor)
1272
      return
1273

    
1274
    # Workaround for a race condition. When DRBD is doing its dance to
1275
    # establish a connection with its peer, it also sends the
1276
    # synchronization speed over the wire. In some cases setting the
1277
    # sync speed only after setting up both sides can race with DRBD
1278
    # connecting, hence we set it here before telling DRBD anything
1279
    # about its peer.
1280
    cls._SetMinorSyncSpeed(minor, constants.SYNC_SPEED)
1281

    
1282
    args = ["drbdsetup", cls._DevPath(minor), "net",
1283
            "%s:%s" % (lhost, lport), "%s:%s" % (rhost, rport), protocol,
1284
            "-A", "discard-zero-changes",
1285
            "-B", "consensus",
1286
            "--create-device",
1287
            ]
1288
    if dual_pri:
1289
      args.append("-m")
1290
    if hmac and secret:
1291
      args.extend(["-a", hmac, "-x", secret])
1292
    result = utils.RunCmd(args)
1293
    if result.failed:
1294
      _ThrowError("drbd%d: can't setup network: %s - %s",
1295
                  minor, result.fail_reason, result.output)
1296

    
1297
    def _CheckNetworkConfig():
1298
      info = cls._GetDevInfo(cls._GetShowData(minor))
1299
      if not "local_addr" in info or not "remote_addr" in info:
1300
        raise utils.RetryAgain()
1301

    
1302
      if (info["local_addr"] != (lhost, lport) or
1303
          info["remote_addr"] != (rhost, rport)):
1304
        raise utils.RetryAgain()
1305

    
1306
    try:
1307
      utils.Retry(_CheckNetworkConfig, 1.0, 10.0)
1308
    except utils.RetryTimeout:
1309
      _ThrowError("drbd%d: timeout while configuring network", minor)
1310

    
1311
  def AddChildren(self, devices):
1312
    """Add a disk to the DRBD device.
1313

1314
    """
1315
    if self.minor is None:
1316
      _ThrowError("drbd%d: can't attach to dbrd8 during AddChildren",
1317
                  self._aminor)
1318
    if len(devices) != 2:
1319
      _ThrowError("drbd%d: need two devices for AddChildren", self.minor)
1320
    info = self._GetDevInfo(self._GetShowData(self.minor))
1321
    if "local_dev" in info:
1322
      _ThrowError("drbd%d: already attached to a local disk", self.minor)
1323
    backend, meta = devices
1324
    if backend.dev_path is None or meta.dev_path is None:
1325
      _ThrowError("drbd%d: children not ready during AddChildren", self.minor)
1326
    backend.Open()
1327
    meta.Open()
1328
    self._CheckMetaSize(meta.dev_path)
1329
    self._InitMeta(self._FindUnusedMinor(), meta.dev_path)
1330

    
1331
    self._AssembleLocal(self.minor, backend.dev_path, meta.dev_path, self.size)
1332
    self._children = devices
1333

    
1334
  def RemoveChildren(self, devices):
1335
    """Detach the drbd device from local storage.
1336

1337
    """
1338
    if self.minor is None:
1339
      _ThrowError("drbd%d: can't attach to drbd8 during RemoveChildren",
1340
                  self._aminor)
1341
    # early return if we don't actually have backing storage
1342
    info = self._GetDevInfo(self._GetShowData(self.minor))
1343
    if "local_dev" not in info:
1344
      return
1345
    if len(self._children) != 2:
1346
      _ThrowError("drbd%d: we don't have two children: %s", self.minor,
1347
                  self._children)
1348
    if self._children.count(None) == 2: # we don't actually have children :)
1349
      logging.warning("drbd%d: requested detach while detached", self.minor)
1350
      return
1351
    if len(devices) != 2:
1352
      _ThrowError("drbd%d: we need two children in RemoveChildren", self.minor)
1353
    for child, dev in zip(self._children, devices):
1354
      if dev != child.dev_path:
1355
        _ThrowError("drbd%d: mismatch in local storage (%s != %s) in"
1356
                    " RemoveChildren", self.minor, dev, child.dev_path)
1357

    
1358
    self._ShutdownLocal(self.minor)
1359
    self._children = []
1360

    
1361
  @classmethod
1362
  def _SetMinorSyncSpeed(cls, minor, kbytes):
1363
    """Set the speed of the DRBD syncer.
1364

1365
    This is the low-level implementation.
1366

1367
    @type minor: int
1368
    @param minor: the drbd minor whose settings we change
1369
    @type kbytes: int
1370
    @param kbytes: the speed in kbytes/second
1371
    @rtype: boolean
1372
    @return: the success of the operation
1373

1374
    """
1375
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "syncer",
1376
                           "-r", "%d" % kbytes, "--create-device"])
1377
    if result.failed:
1378
      logging.error("Can't change syncer rate: %s - %s",
1379
                    result.fail_reason, result.output)
1380
    return not result.failed
1381

    
1382
  def SetSyncSpeed(self, kbytes):
1383
    """Set the speed of the DRBD syncer.
1384

1385
    @type kbytes: int
1386
    @param kbytes: the speed in kbytes/second
1387
    @rtype: boolean
1388
    @return: the success of the operation
1389

1390
    """
1391
    if self.minor is None:
1392
      logging.info("Not attached during SetSyncSpeed")
1393
      return False
1394
    children_result = super(DRBD8, self).SetSyncSpeed(kbytes)
1395
    return self._SetMinorSyncSpeed(self.minor, kbytes) and children_result
1396

    
1397
  def GetProcStatus(self):
1398
    """Return device data from /proc.
1399

1400
    """
1401
    if self.minor is None:
1402
      _ThrowError("drbd%d: GetStats() called while not attached", self._aminor)
1403
    proc_info = self._MassageProcData(self._GetProcData())
1404
    if self.minor not in proc_info:
1405
      _ThrowError("drbd%d: can't find myself in /proc", self.minor)
1406
    return DRBD8Status(proc_info[self.minor])
1407

    
1408
  def GetSyncStatus(self):
1409
    """Returns the sync status of the device.
1410

1411

1412
    If sync_percent is None, it means all is ok
1413
    If estimated_time is None, it means we can't estimate
1414
    the time needed, otherwise it's the time left in seconds.
1415

1416

1417
    We set the is_degraded parameter to True on two conditions:
1418
    network not connected or local disk missing.
1419

1420
    We compute the ldisk parameter based on whether we have a local
1421
    disk or not.
1422

1423
    @rtype: objects.BlockDevStatus
1424

1425
    """
1426
    if self.minor is None and not self.Attach():
1427
      _ThrowError("drbd%d: can't Attach() in GetSyncStatus", self._aminor)
1428

    
1429
    stats = self.GetProcStatus()
1430
    is_degraded = not stats.is_connected or not stats.is_disk_uptodate
1431

    
1432
    if stats.is_disk_uptodate:
1433
      ldisk_status = constants.LDS_OKAY
1434
    elif stats.is_diskless:
1435
      ldisk_status = constants.LDS_FAULTY
1436
    else:
1437
      ldisk_status = constants.LDS_UNKNOWN
1438

    
1439
    return objects.BlockDevStatus(dev_path=self.dev_path,
1440
                                  major=self.major,
1441
                                  minor=self.minor,
1442
                                  sync_percent=stats.sync_percent,
1443
                                  estimated_time=stats.est_time,
1444
                                  is_degraded=is_degraded,
1445
                                  ldisk_status=ldisk_status)
1446

    
1447
  def Open(self, force=False):
1448
    """Make the local state primary.
1449

1450
    If the 'force' parameter is given, the '-o' option is passed to
1451
    drbdsetup. Since this is a potentially dangerous operation, the
1452
    force flag should be only given after creation, when it actually
1453
    is mandatory.
1454

1455
    """
1456
    if self.minor is None and not self.Attach():
1457
      logging.error("DRBD cannot attach to a device during open")
1458
      return False
1459
    cmd = ["drbdsetup", self.dev_path, "primary"]
1460
    if force:
1461
      cmd.append("-o")
1462
    result = utils.RunCmd(cmd)
1463
    if result.failed:
1464
      _ThrowError("drbd%d: can't make drbd device primary: %s", self.minor,
1465
                  result.output)
1466

    
1467
  def Close(self):
1468
    """Make the local state secondary.
1469

1470
    This will, of course, fail if the device is in use.
1471

1472
    """
1473
    if self.minor is None and not self.Attach():
1474
      _ThrowError("drbd%d: can't Attach() in Close()", self._aminor)
1475
    result = utils.RunCmd(["drbdsetup", self.dev_path, "secondary"])
1476
    if result.failed:
1477
      _ThrowError("drbd%d: can't switch drbd device to secondary: %s",
1478
                  self.minor, result.output)
1479

    
1480
  def DisconnectNet(self):
1481
    """Removes network configuration.
1482

1483
    This method shutdowns the network side of the device.
1484

1485
    The method will wait up to a hardcoded timeout for the device to
1486
    go into standalone after the 'disconnect' command before
1487
    re-configuring it, as sometimes it takes a while for the
1488
    disconnect to actually propagate and thus we might issue a 'net'
1489
    command while the device is still connected. If the device will
1490
    still be attached to the network and we time out, we raise an
1491
    exception.
1492

1493
    """
1494
    if self.minor is None:
1495
      _ThrowError("drbd%d: disk not attached in re-attach net", self._aminor)
1496

    
1497
    if None in (self._lhost, self._lport, self._rhost, self._rport):
1498
      _ThrowError("drbd%d: DRBD disk missing network info in"
1499
                  " DisconnectNet()", self.minor)
1500

    
1501
    class _DisconnectStatus:
1502
      def __init__(self, ever_disconnected):
1503
        self.ever_disconnected = ever_disconnected
1504

    
1505
    dstatus = _DisconnectStatus(_IgnoreError(self._ShutdownNet, self.minor))
1506

    
1507
    def _WaitForDisconnect():
1508
      if self.GetProcStatus().is_standalone:
1509
        return
1510

    
1511
      # retry the disconnect, it seems possible that due to a well-time
1512
      # disconnect on the peer, my disconnect command might be ignored and
1513
      # forgotten
1514
      dstatus.ever_disconnected = \
1515
        _IgnoreError(self._ShutdownNet, self.minor) or dstatus.ever_disconnected
1516

    
1517
      raise utils.RetryAgain()
1518

    
1519
    # Keep start time
1520
    start_time = time.time()
1521

    
1522
    try:
1523
      # Start delay at 100 milliseconds and grow up to 2 seconds
1524
      utils.Retry(_WaitForDisconnect, (0.1, 1.5, 2.0),
1525
                  self._NET_RECONFIG_TIMEOUT)
1526
    except utils.RetryTimeout:
1527
      if dstatus.ever_disconnected:
1528
        msg = ("drbd%d: device did not react to the"
1529
               " 'disconnect' command in a timely manner")
1530
      else:
1531
        msg = "drbd%d: can't shutdown network, even after multiple retries"
1532

    
1533
      _ThrowError(msg, self.minor)
1534

    
1535
    reconfig_time = time.time() - start_time
1536
    if reconfig_time > (self._NET_RECONFIG_TIMEOUT * 0.25):
1537
      logging.info("drbd%d: DisconnectNet: detach took %.3f seconds",
1538
                   self.minor, reconfig_time)
1539

    
1540
  def AttachNet(self, multimaster):
1541
    """Reconnects the network.
1542

1543
    This method connects the network side of the device with a
1544
    specified multi-master flag. The device needs to be 'Standalone'
1545
    but have valid network configuration data.
1546

1547
    Args:
1548
      - multimaster: init the network in dual-primary mode
1549

1550
    """
1551
    if self.minor is None:
1552
      _ThrowError("drbd%d: device not attached in AttachNet", self._aminor)
1553

    
1554
    if None in (self._lhost, self._lport, self._rhost, self._rport):
1555
      _ThrowError("drbd%d: missing network info in AttachNet()", self.minor)
1556

    
1557
    status = self.GetProcStatus()
1558

    
1559
    if not status.is_standalone:
1560
      _ThrowError("drbd%d: device is not standalone in AttachNet", self.minor)
1561

    
1562
    self._AssembleNet(self.minor,
1563
                      (self._lhost, self._lport, self._rhost, self._rport),
1564
                      constants.DRBD_NET_PROTOCOL, dual_pri=multimaster,
1565
                      hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1566

    
1567
  def Attach(self):
1568
    """Check if our minor is configured.
1569

1570
    This doesn't do any device configurations - it only checks if the
1571
    minor is in a state different from Unconfigured.
1572

1573
    Note that this function will not change the state of the system in
1574
    any way (except in case of side-effects caused by reading from
1575
    /proc).
1576

1577
    """
1578
    used_devs = self.GetUsedDevs()
1579
    if self._aminor in used_devs:
1580
      minor = self._aminor
1581
    else:
1582
      minor = None
1583

    
1584
    self._SetFromMinor(minor)
1585
    return minor is not None
1586

    
1587
  def Assemble(self):
1588
    """Assemble the drbd.
1589

1590
    Method:
1591
      - if we have a configured device, we try to ensure that it matches
1592
        our config
1593
      - if not, we create it from zero
1594

1595
    """
1596
    super(DRBD8, self).Assemble()
1597

    
1598
    self.Attach()
1599
    if self.minor is None:
1600
      # local device completely unconfigured
1601
      self._FastAssemble()
1602
    else:
1603
      # we have to recheck the local and network status and try to fix
1604
      # the device
1605
      self._SlowAssemble()
1606

    
1607
  def _SlowAssemble(self):
1608
    """Assembles the DRBD device from a (partially) configured device.
1609

1610
    In case of partially attached (local device matches but no network
1611
    setup), we perform the network attach. If successful, we re-test
1612
    the attach if can return success.
1613

1614
    """
1615
    # TODO: Rewrite to not use a for loop just because there is 'break'
1616
    # pylint: disable-msg=W0631
1617
    net_data = (self._lhost, self._lport, self._rhost, self._rport)
1618
    for minor in (self._aminor,):
1619
      info = self._GetDevInfo(self._GetShowData(minor))
1620
      match_l = self._MatchesLocal(info)
1621
      match_r = self._MatchesNet(info)
1622

    
1623
      if match_l and match_r:
1624
        # everything matches
1625
        break
1626

    
1627
      if match_l and not match_r and "local_addr" not in info:
1628
        # disk matches, but not attached to network, attach and recheck
1629
        self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
1630
                          hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1631
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1632
          break
1633
        else:
1634
          _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
1635
                      " show' disagrees", minor)
1636

    
1637
      if match_r and "local_dev" not in info:
1638
        # no local disk, but network attached and it matches
1639
        self._AssembleLocal(minor, self._children[0].dev_path,
1640
                            self._children[1].dev_path, self.size)
1641
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1642
          break
1643
        else:
1644
          _ThrowError("drbd%d: disk attach successful, but 'drbdsetup"
1645
                      " show' disagrees", minor)
1646

    
1647
      # this case must be considered only if we actually have local
1648
      # storage, i.e. not in diskless mode, because all diskless
1649
      # devices are equal from the point of view of local
1650
      # configuration
1651
      if (match_l and "local_dev" in info and
1652
          not match_r and "local_addr" in info):
1653
        # strange case - the device network part points to somewhere
1654
        # else, even though its local storage is ours; as we own the
1655
        # drbd space, we try to disconnect from the remote peer and
1656
        # reconnect to our correct one
1657
        try:
1658
          self._ShutdownNet(minor)
1659
        except errors.BlockDeviceError, err:
1660
          _ThrowError("drbd%d: device has correct local storage, wrong"
1661
                      " remote peer and is unable to disconnect in order"
1662
                      " to attach to the correct peer: %s", minor, str(err))
1663
        # note: _AssembleNet also handles the case when we don't want
1664
        # local storage (i.e. one or more of the _[lr](host|port) is
1665
        # None)
1666
        self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
1667
                          hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1668
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1669
          break
1670
        else:
1671
          _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
1672
                      " show' disagrees", minor)
1673

    
1674
    else:
1675
      minor = None
1676

    
1677
    self._SetFromMinor(minor)
1678
    if minor is None:
1679
      _ThrowError("drbd%d: cannot activate, unknown or unhandled reason",
1680
                  self._aminor)
1681

    
1682
  def _FastAssemble(self):
1683
    """Assemble the drbd device from zero.
1684

1685
    This is run when in Assemble we detect our minor is unused.
1686

1687
    """
1688
    minor = self._aminor
1689
    if self._children and self._children[0] and self._children[1]:
1690
      self._AssembleLocal(minor, self._children[0].dev_path,
1691
                          self._children[1].dev_path, self.size)
1692
    if self._lhost and self._lport and self._rhost and self._rport:
1693
      self._AssembleNet(minor,
1694
                        (self._lhost, self._lport, self._rhost, self._rport),
1695
                        constants.DRBD_NET_PROTOCOL,
1696
                        hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1697
    self._SetFromMinor(minor)
1698

    
1699
  @classmethod
1700
  def _ShutdownLocal(cls, minor):
1701
    """Detach from the local device.
1702

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

1706
    """
1707
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "detach"])
1708
    if result.failed:
1709
      _ThrowError("drbd%d: can't detach local disk: %s", minor, result.output)
1710

    
1711
  @classmethod
1712
  def _ShutdownNet(cls, minor):
1713
    """Disconnect from the remote peer.
1714

1715
    This fails if we don't have a local device.
1716

1717
    """
1718
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disconnect"])
1719
    if result.failed:
1720
      _ThrowError("drbd%d: can't shutdown network: %s", minor, result.output)
1721

    
1722
  @classmethod
1723
  def _ShutdownAll(cls, minor):
1724
    """Deactivate the device.
1725

1726
    This will, of course, fail if the device is in use.
1727

1728
    """
1729
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "down"])
1730
    if result.failed:
1731
      _ThrowError("drbd%d: can't shutdown drbd device: %s",
1732
                  minor, result.output)
1733

    
1734
  def Shutdown(self):
1735
    """Shutdown the DRBD device.
1736

1737
    """
1738
    if self.minor is None and not self.Attach():
1739
      logging.info("drbd%d: not attached during Shutdown()", self._aminor)
1740
      return
1741
    minor = self.minor
1742
    self.minor = None
1743
    self.dev_path = None
1744
    self._ShutdownAll(minor)
1745

    
1746
  def Remove(self):
1747
    """Stub remove for DRBD devices.
1748

1749
    """
1750
    self.Shutdown()
1751

    
1752
  @classmethod
1753
  def Create(cls, unique_id, children, size):
1754
    """Create a new DRBD8 device.
1755

1756
    Since DRBD devices are not created per se, just assembled, this
1757
    function only initializes the metadata.
1758

1759
    """
1760
    if len(children) != 2:
1761
      raise errors.ProgrammerError("Invalid setup for the drbd device")
1762
    # check that the minor is unused
1763
    aminor = unique_id[4]
1764
    proc_info = cls._MassageProcData(cls._GetProcData())
1765
    if aminor in proc_info:
1766
      status = DRBD8Status(proc_info[aminor])
1767
      in_use = status.is_in_use
1768
    else:
1769
      in_use = False
1770
    if in_use:
1771
      _ThrowError("drbd%d: minor is already in use at Create() time", aminor)
1772
    meta = children[1]
1773
    meta.Assemble()
1774
    if not meta.Attach():
1775
      _ThrowError("drbd%d: can't attach to meta device '%s'",
1776
                  aminor, meta)
1777
    cls._CheckMetaSize(meta.dev_path)
1778
    cls._InitMeta(aminor, meta.dev_path)
1779
    return cls(unique_id, children, size)
1780

    
1781
  def Grow(self, amount):
1782
    """Resize the DRBD device and its backing storage.
1783

1784
    """
1785
    if self.minor is None:
1786
      _ThrowError("drbd%d: Grow called while not attached", self._aminor)
1787
    if len(self._children) != 2 or None in self._children:
1788
      _ThrowError("drbd%d: cannot grow diskless device", self.minor)
1789
    self._children[0].Grow(amount)
1790
    result = utils.RunCmd(["drbdsetup", self.dev_path, "resize", "-s",
1791
                           "%dm" % (self.size + amount)])
1792
    if result.failed:
1793
      _ThrowError("drbd%d: resize failed: %s", self.minor, result.output)
1794

    
1795

    
1796
class FileStorage(BlockDev):
1797
  """File device.
1798

1799
  This class represents the a file storage backend device.
1800

1801
  The unique_id for the file device is a (file_driver, file_path) tuple.
1802

1803
  """
1804
  def __init__(self, unique_id, children, size):
1805
    """Initalizes a file device backend.
1806

1807
    """
1808
    if children:
1809
      raise errors.BlockDeviceError("Invalid setup for file device")
1810
    super(FileStorage, self).__init__(unique_id, children, size)
1811
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1812
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1813
    self.driver = unique_id[0]
1814
    self.dev_path = unique_id[1]
1815
    self.Attach()
1816

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

1820
    Checks whether the file device exists, raises BlockDeviceError otherwise.
1821

1822
    """
1823
    if not os.path.exists(self.dev_path):
1824
      _ThrowError("File device '%s' does not exist" % self.dev_path)
1825

    
1826
  def Shutdown(self):
1827
    """Shutdown the device.
1828

1829
    This is a no-op for the file type, as we don't deactivate
1830
    the file on shutdown.
1831

1832
    """
1833
    pass
1834

    
1835
  def Open(self, force=False):
1836
    """Make the device ready for I/O.
1837

1838
    This is a no-op for the file type.
1839

1840
    """
1841
    pass
1842

    
1843
  def Close(self):
1844
    """Notifies that the device will no longer be used for I/O.
1845

1846
    This is a no-op for the file type.
1847

1848
    """
1849
    pass
1850

    
1851
  def Remove(self):
1852
    """Remove the file backing the block device.
1853

1854
    @rtype: boolean
1855
    @return: True if the removal was successful
1856

1857
    """
1858
    try:
1859
      os.remove(self.dev_path)
1860
    except OSError, err:
1861
      if err.errno != errno.ENOENT:
1862
        _ThrowError("Can't remove file '%s': %s", self.dev_path, err)
1863

    
1864
  def Rename(self, new_id):
1865
    """Renames the file.
1866

1867
    """
1868
    # TODO: implement rename for file-based storage
1869
    _ThrowError("Rename is not supported for file-based storage")
1870

    
1871
  def Grow(self, amount):
1872
    """Grow the file
1873

1874
    @param amount: the amount (in mebibytes) to grow with
1875

1876
    """
1877
    # TODO: implement grow for file-based storage
1878
    _ThrowError("Grow not supported for file-based storage")
1879

    
1880
  def Attach(self):
1881
    """Attach to an existing file.
1882

1883
    Check if this file already exists.
1884

1885
    @rtype: boolean
1886
    @return: True if file exists
1887

1888
    """
1889
    self.attached = os.path.exists(self.dev_path)
1890
    return self.attached
1891

    
1892
  def GetActualSize(self):
1893
    """Return the actual disk size.
1894

1895
    @note: the device needs to be active when this is called
1896

1897
    """
1898
    assert self.attached, "BlockDevice not attached in GetActualSize()"
1899
    try:
1900
      st = os.stat(self.dev_path)
1901
      return st.st_size
1902
    except OSError, err:
1903
      _ThrowError("Can't stat %s: %s", self.dev_path, err)
1904

    
1905
  @classmethod
1906
  def Create(cls, unique_id, children, size):
1907
    """Create a new file.
1908

1909
    @param size: the size of file in MiB
1910

1911
    @rtype: L{bdev.FileStorage}
1912
    @return: an instance of FileStorage
1913

1914
    """
1915
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1916
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1917
    dev_path = unique_id[1]
1918
    try:
1919
      fd = os.open(dev_path, os.O_RDWR | os.O_CREAT | os.O_EXCL)
1920
      f = os.fdopen(fd, "w")
1921
      f.truncate(size * 1024 * 1024)
1922
      f.close()
1923
    except EnvironmentError, err:
1924
      if err.errno == errno.EEXIST:
1925
        _ThrowError("File already existing: %s", dev_path)
1926
      _ThrowError("Error in file creation: %", str(err))
1927

    
1928
    return FileStorage(unique_id, children, size)
1929

    
1930

    
1931
DEV_MAP = {
1932
  constants.LD_LV: LogicalVolume,
1933
  constants.LD_DRBD8: DRBD8,
1934
  }
1935

    
1936
if constants.ENABLE_FILE_STORAGE:
1937
  DEV_MAP[constants.LD_FILE] = FileStorage
1938

    
1939

    
1940
def FindDevice(dev_type, unique_id, children, size):
1941
  """Search for an existing, assembled device.
1942

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

1946
  """
1947
  if dev_type not in DEV_MAP:
1948
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1949
  device = DEV_MAP[dev_type](unique_id, children, size)
1950
  if not device.attached:
1951
    return None
1952
  return device
1953

    
1954

    
1955
def Assemble(dev_type, unique_id, children, size):
1956
  """Try to attach or assemble an existing device.
1957

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

1961
  """
1962
  if dev_type not in DEV_MAP:
1963
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1964
  device = DEV_MAP[dev_type](unique_id, children, size)
1965
  device.Assemble()
1966
  return device
1967

    
1968

    
1969
def Create(dev_type, unique_id, children, size):
1970
  """Create a device.
1971

1972
  """
1973
  if dev_type not in DEV_MAP:
1974
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1975
  device = DEV_MAP[dev_type].Create(unique_id, children, size)
1976
  return device