Statistics
| Branch: | Tag: | Revision:

root / lib / bdev.py @ ee844e20

History | View | Annotate | Download (62.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
  def __init__(self, unique_id, children, size):
353
    """Attaches to a LV device.
354

355
    The unique_id is a tuple (vg_name, lv_name)
356

357
    """
358
    super(LogicalVolume, self).__init__(unique_id, children, size)
359
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
360
      raise ValueError("Invalid configuration data %s" % str(unique_id))
361
    self._vg_name, self._lv_name = unique_id
362
    self.dev_path = "/dev/%s/%s" % (self._vg_name, self._lv_name)
363
    self._degraded = True
364
    self.major = self.minor = self.pe_size = self.stripe_count = None
365
    self.Attach()
366

    
367
  @classmethod
368
  def Create(cls, unique_id, children, size):
369
    """Create a new logical volume.
370

371
    """
372
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
373
      raise errors.ProgrammerError("Invalid configuration data %s" %
374
                                   str(unique_id))
375
    vg_name, lv_name = unique_id
376
    pvs_info = cls.GetPVInfo([vg_name])
377
    if not pvs_info:
378
      _ThrowError("Can't compute PV info for vg %s", vg_name)
379
    pvs_info.sort()
380
    pvs_info.reverse()
381

    
382
    pvlist = [ pv[1] for pv in pvs_info ]
383
    if utils.any(pvlist, lambda v: ":" in v):
384
      _ThrowError("Some of your PVs have invalid character ':'"
385
                  " in their name")
386
    free_size = sum([ pv[0] for pv in pvs_info ])
387
    current_pvs = len(pvlist)
388
    stripes = min(current_pvs, constants.LVM_STRIPECOUNT)
389

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

    
409
  @staticmethod
410
  def GetPVInfo(vg_names, filter_allocatable=True):
411
    """Get the free space info for PVs in a volume group.
412

413
    @param vg_names: list of volume group names, if empty all will be returned
414
    @param filter_allocatable: whether to skip over unallocatable PVs
415

416
    @rtype: list
417
    @return: list of tuples (free_space, name) with free_space in mebibytes
418

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

    
443
    return data
444

    
445
  def Remove(self):
446
    """Remove this logical volume.
447

448
    """
449
    if not self.minor and not self.Attach():
450
      # the LV does not exist
451
      return
452
    result = utils.RunCmd(["lvremove", "-f", "%s/%s" %
453
                           (self._vg_name, self._lv_name)])
454
    if result.failed:
455
      _ThrowError("Can't lvremove: %s - %s", result.fail_reason, result.output)
456

    
457
  def Rename(self, new_id):
458
    """Rename this logical volume.
459

460
    """
461
    if not isinstance(new_id, (tuple, list)) or len(new_id) != 2:
462
      raise errors.ProgrammerError("Invalid new logical id '%s'" % new_id)
463
    new_vg, new_name = new_id
464
    if new_vg != self._vg_name:
465
      raise errors.ProgrammerError("Can't move a logical volume across"
466
                                   " volume groups (from %s to to %s)" %
467
                                   (self._vg_name, new_vg))
468
    result = utils.RunCmd(["lvrename", new_vg, self._lv_name, new_name])
469
    if result.failed:
470
      _ThrowError("Failed to rename the logical volume: %s", result.output)
471
    self._lv_name = new_name
472
    self.dev_path = "/dev/%s/%s" % (self._vg_name, self._lv_name)
473

    
474
  def Attach(self):
475
    """Attach to an existing LV.
476

477
    This method will try to see if an existing and active LV exists
478
    which matches our name. If so, its major/minor will be
479
    recorded.
480

481
    """
482
    self.attached = False
483
    result = utils.RunCmd(["lvs", "--noheadings", "--separator=,",
484
                           "--units=m", "--nosuffix",
485
                           "-olv_attr,lv_kernel_major,lv_kernel_minor,"
486
                           "vg_extent_size,stripes", self.dev_path])
487
    if result.failed:
488
      logging.error("Can't find LV %s: %s, %s",
489
                    self.dev_path, result.fail_reason, result.output)
490
      return False
491
    # the output can (and will) have multiple lines for multi-segment
492
    # LVs, as the 'stripes' parameter is a segment one, so we take
493
    # only the last entry, which is the one we're interested in; note
494
    # that with LVM2 anyway the 'stripes' value must be constant
495
    # across segments, so this is a no-op actually
496
    out = result.stdout.splitlines()
497
    if not out: # totally empty result? splitlines() returns at least
498
                # one line for any non-empty string
499
      logging.error("Can't parse LVS output, no lines? Got '%s'", str(out))
500
      return False
501
    out = out[-1].strip().rstrip(',')
502
    out = out.split(",")
503
    if len(out) != 5:
504
      logging.error("Can't parse LVS output, len(%s) != 5", str(out))
505
      return False
506

    
507
    status, major, minor, pe_size, stripes = out
508
    if len(status) != 6:
509
      logging.error("lvs lv_attr is not 6 characters (%s)", status)
510
      return False
511

    
512
    try:
513
      major = int(major)
514
      minor = int(minor)
515
    except (TypeError, ValueError), err:
516
      logging.error("lvs major/minor cannot be parsed: %s", str(err))
517

    
518
    try:
519
      pe_size = int(float(pe_size))
520
    except (TypeError, ValueError), err:
521
      logging.error("Can't parse vg extent size: %s", err)
522
      return False
523

    
524
    try:
525
      stripes = int(stripes)
526
    except (TypeError, ValueError), err:
527
      logging.error("Can't parse the number of stripes: %s", err)
528
      return False
529

    
530
    self.major = major
531
    self.minor = minor
532
    self.pe_size = pe_size
533
    self.stripe_count = stripes
534
    self._degraded = status[0] == 'v' # virtual volume, i.e. doesn't backing
535
                                      # storage
536
    self.attached = True
537
    return True
538

    
539
  def Assemble(self):
540
    """Assemble the device.
541

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

546
    """
547
    result = utils.RunCmd(["lvchange", "-ay", self.dev_path])
548
    if result.failed:
549
      _ThrowError("Can't activate lv %s: %s", self.dev_path, result.output)
550

    
551
  def Shutdown(self):
552
    """Shutdown the device.
553

554
    This is a no-op for the LV device type, as we don't deactivate the
555
    volumes on shutdown.
556

557
    """
558
    pass
559

    
560
  def GetSyncStatus(self):
561
    """Returns the sync status of the device.
562

563
    If this device is a mirroring device, this function returns the
564
    status of the mirror.
565

566
    For logical volumes, sync_percent and estimated_time are always
567
    None (no recovery in progress, as we don't handle the mirrored LV
568
    case). The is_degraded parameter is the inverse of the ldisk
569
    parameter.
570

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

577
    The status was already read in Attach, so we just return it.
578

579
    @rtype: objects.BlockDevStatus
580

581
    """
582
    if self._degraded:
583
      ldisk_status = constants.LDS_FAULTY
584
    else:
585
      ldisk_status = constants.LDS_OKAY
586

    
587
    return objects.BlockDevStatus(dev_path=self.dev_path,
588
                                  major=self.major,
589
                                  minor=self.minor,
590
                                  sync_percent=None,
591
                                  estimated_time=None,
592
                                  is_degraded=self._degraded,
593
                                  ldisk_status=ldisk_status)
594

    
595
  def Open(self, force=False):
596
    """Make the device ready for I/O.
597

598
    This is a no-op for the LV device type.
599

600
    """
601
    pass
602

    
603
  def Close(self):
604
    """Notifies that the device will no longer be used for I/O.
605

606
    This is a no-op for the LV device type.
607

608
    """
609
    pass
610

    
611
  def Snapshot(self, size):
612
    """Create a snapshot copy of an lvm block device.
613

614
    """
615
    snap_name = self._lv_name + ".snap"
616

    
617
    # remove existing snapshot if found
618
    snap = LogicalVolume((self._vg_name, snap_name), None, size)
619
    _IgnoreError(snap.Remove)
620

    
621
    pvs_info = self.GetPVInfo([self._vg_name])
622
    if not pvs_info:
623
      _ThrowError("Can't compute PV info for vg %s", self._vg_name)
624
    pvs_info.sort()
625
    pvs_info.reverse()
626
    free_size, _, _ = pvs_info[0]
627
    if free_size < size:
628
      _ThrowError("Not enough free space: required %s,"
629
                  " available %s", size, free_size)
630

    
631
    result = utils.RunCmd(["lvcreate", "-L%dm" % size, "-s",
632
                           "-n%s" % snap_name, self.dev_path])
633
    if result.failed:
634
      _ThrowError("command: %s error: %s - %s",
635
                  result.cmd, result.fail_reason, result.output)
636

    
637
    return snap_name
638

    
639
  def SetInfo(self, text):
640
    """Update metadata with info text.
641

642
    """
643
    BlockDev.SetInfo(self, text)
644

    
645
    # Replace invalid characters
646
    text = re.sub('^[^A-Za-z0-9_+.]', '_', text)
647
    text = re.sub('[^-A-Za-z0-9_+.]', '_', text)
648

    
649
    # Only up to 128 characters are allowed
650
    text = text[:128]
651

    
652
    result = utils.RunCmd(["lvchange", "--addtag", text,
653
                           self.dev_path])
654
    if result.failed:
655
      _ThrowError("Command: %s error: %s - %s", result.cmd, result.fail_reason,
656
                  result.output)
657

    
658
  def Grow(self, amount):
659
    """Grow the logical volume.
660

661
    """
662
    if self.pe_size is None or self.stripe_count is None:
663
      if not self.Attach():
664
        _ThrowError("Can't attach to LV during Grow()")
665
    full_stripe_size = self.pe_size * self.stripe_count
666
    rest = amount % full_stripe_size
667
    if rest != 0:
668
      amount += full_stripe_size - rest
669
    # we try multiple algorithms since the 'best' ones might not have
670
    # space available in the right place, but later ones might (since
671
    # they have less constraints); also note that only recent LVM
672
    # supports 'cling'
673
    for alloc_policy in "contiguous", "cling", "normal":
674
      result = utils.RunCmd(["lvextend", "--alloc", alloc_policy,
675
                             "-L", "+%dm" % amount, self.dev_path])
676
      if not result.failed:
677
        return
678
    _ThrowError("Can't grow LV %s: %s", self.dev_path, result.output)
679

    
680

    
681
class DRBD8Status(object):
682
  """A DRBD status representation class.
683

684
  Note that this doesn't support unconfigured devices (cs:Unconfigured).
685

686
  """
687
  UNCONF_RE = re.compile(r"\s*[0-9]+:\s*cs:Unconfigured$")
688
  LINE_RE = re.compile(r"\s*[0-9]+:\s*cs:(\S+)\s+(?:st|ro):([^/]+)/(\S+)"
689
                       "\s+ds:([^/]+)/(\S+)\s+.*$")
690
  SYNC_RE = re.compile(r"^.*\ssync'ed:\s*([0-9.]+)%.*"
691
                       "\sfinish: ([0-9]+):([0-9]+):([0-9]+)\s.*$")
692

    
693
  CS_UNCONFIGURED = "Unconfigured"
694
  CS_STANDALONE = "StandAlone"
695
  CS_WFCONNECTION = "WFConnection"
696
  CS_WFREPORTPARAMS = "WFReportParams"
697
  CS_CONNECTED = "Connected"
698
  CS_STARTINGSYNCS = "StartingSyncS"
699
  CS_STARTINGSYNCT = "StartingSyncT"
700
  CS_WFBITMAPS = "WFBitMapS"
701
  CS_WFBITMAPT = "WFBitMapT"
702
  CS_WFSYNCUUID = "WFSyncUUID"
703
  CS_SYNCSOURCE = "SyncSource"
704
  CS_SYNCTARGET = "SyncTarget"
705
  CS_PAUSEDSYNCS = "PausedSyncS"
706
  CS_PAUSEDSYNCT = "PausedSyncT"
707
  CSET_SYNC = frozenset([
708
    CS_WFREPORTPARAMS,
709
    CS_STARTINGSYNCS,
710
    CS_STARTINGSYNCT,
711
    CS_WFBITMAPS,
712
    CS_WFBITMAPT,
713
    CS_WFSYNCUUID,
714
    CS_SYNCSOURCE,
715
    CS_SYNCTARGET,
716
    CS_PAUSEDSYNCS,
717
    CS_PAUSEDSYNCT,
718
    ])
719

    
720
  DS_DISKLESS = "Diskless"
721
  DS_ATTACHING = "Attaching" # transient state
722
  DS_FAILED = "Failed" # transient state, next: diskless
723
  DS_NEGOTIATING = "Negotiating" # transient state
724
  DS_INCONSISTENT = "Inconsistent" # while syncing or after creation
725
  DS_OUTDATED = "Outdated"
726
  DS_DUNKNOWN = "DUnknown" # shown for peer disk when not connected
727
  DS_CONSISTENT = "Consistent"
728
  DS_UPTODATE = "UpToDate" # normal state
729

    
730
  RO_PRIMARY = "Primary"
731
  RO_SECONDARY = "Secondary"
732
  RO_UNKNOWN = "Unknown"
733

    
734
  def __init__(self, procline):
735
    u = self.UNCONF_RE.match(procline)
736
    if u:
737
      self.cstatus = self.CS_UNCONFIGURED
738
      self.lrole = self.rrole = self.ldisk = self.rdisk = None
739
    else:
740
      m = self.LINE_RE.match(procline)
741
      if not m:
742
        raise errors.BlockDeviceError("Can't parse input data '%s'" % procline)
743
      self.cstatus = m.group(1)
744
      self.lrole = m.group(2)
745
      self.rrole = m.group(3)
746
      self.ldisk = m.group(4)
747
      self.rdisk = m.group(5)
748

    
749
    # end reading of data from the LINE_RE or UNCONF_RE
750

    
751
    self.is_standalone = self.cstatus == self.CS_STANDALONE
752
    self.is_wfconn = self.cstatus == self.CS_WFCONNECTION
753
    self.is_connected = self.cstatus == self.CS_CONNECTED
754
    self.is_primary = self.lrole == self.RO_PRIMARY
755
    self.is_secondary = self.lrole == self.RO_SECONDARY
756
    self.peer_primary = self.rrole == self.RO_PRIMARY
757
    self.peer_secondary = self.rrole == self.RO_SECONDARY
758
    self.both_primary = self.is_primary and self.peer_primary
759
    self.both_secondary = self.is_secondary and self.peer_secondary
760

    
761
    self.is_diskless = self.ldisk == self.DS_DISKLESS
762
    self.is_disk_uptodate = self.ldisk == self.DS_UPTODATE
763

    
764
    self.is_in_resync = self.cstatus in self.CSET_SYNC
765
    self.is_in_use = self.cstatus != self.CS_UNCONFIGURED
766

    
767
    m = self.SYNC_RE.match(procline)
768
    if m:
769
      self.sync_percent = float(m.group(1))
770
      hours = int(m.group(2))
771
      minutes = int(m.group(3))
772
      seconds = int(m.group(4))
773
      self.est_time = hours * 3600 + minutes * 60 + seconds
774
    else:
775
      # we have (in this if branch) no percent information, but if
776
      # we're resyncing we need to 'fake' a sync percent information,
777
      # as this is how cmdlib determines if it makes sense to wait for
778
      # resyncing or not
779
      if self.is_in_resync:
780
        self.sync_percent = 0
781
      else:
782
        self.sync_percent = None
783
      self.est_time = None
784

    
785

    
786
class BaseDRBD(BlockDev): # pylint: disable-msg=W0223
787
  """Base DRBD class.
788

789
  This class contains a few bits of common functionality between the
790
  0.7 and 8.x versions of DRBD.
791

792
  """
793
  _VERSION_RE = re.compile(r"^version: (\d+)\.(\d+)\.(\d+)"
794
                           r" \(api:(\d+)/proto:(\d+)(?:-(\d+))?\)")
795

    
796
  _DRBD_MAJOR = 147
797
  _ST_UNCONFIGURED = "Unconfigured"
798
  _ST_WFCONNECTION = "WFConnection"
799
  _ST_CONNECTED = "Connected"
800

    
801
  _STATUS_FILE = "/proc/drbd"
802

    
803
  @staticmethod
804
  def _GetProcData(filename=_STATUS_FILE):
805
    """Return data from /proc/drbd.
806

807
    """
808
    try:
809
      data = utils.ReadFile(filename).splitlines()
810
    except EnvironmentError, err:
811
      if err.errno == errno.ENOENT:
812
        _ThrowError("The file %s cannot be opened, check if the module"
813
                    " is loaded (%s)", filename, str(err))
814
      else:
815
        _ThrowError("Can't read the DRBD proc file %s: %s", filename, str(err))
816
    if not data:
817
      _ThrowError("Can't read any data from %s", filename)
818
    return data
819

    
820
  @staticmethod
821
  def _MassageProcData(data):
822
    """Transform the output of _GetProdData into a nicer form.
823

824
    @return: a dictionary of minor: joined lines from /proc/drbd
825
        for that minor
826

827
    """
828
    lmatch = re.compile("^ *([0-9]+):.*$")
829
    results = {}
830
    old_minor = old_line = None
831
    for line in data:
832
      if not line: # completely empty lines, as can be returned by drbd8.0+
833
        continue
834
      lresult = lmatch.match(line)
835
      if lresult is not None:
836
        if old_minor is not None:
837
          results[old_minor] = old_line
838
        old_minor = int(lresult.group(1))
839
        old_line = line
840
      else:
841
        if old_minor is not None:
842
          old_line += " " + line.strip()
843
    # add last line
844
    if old_minor is not None:
845
      results[old_minor] = old_line
846
    return results
847

    
848
  @classmethod
849
  def _GetVersion(cls):
850
    """Return the DRBD version.
851

852
    This will return a dict with keys:
853
      - k_major
854
      - k_minor
855
      - k_point
856
      - api
857
      - proto
858
      - proto2 (only on drbd > 8.2.X)
859

860
    """
861
    proc_data = cls._GetProcData()
862
    first_line = proc_data[0].strip()
863
    version = cls._VERSION_RE.match(first_line)
864
    if not version:
865
      raise errors.BlockDeviceError("Can't parse DRBD version from '%s'" %
866
                                    first_line)
867

    
868
    values = version.groups()
869
    retval = {'k_major': int(values[0]),
870
              'k_minor': int(values[1]),
871
              'k_point': int(values[2]),
872
              'api': int(values[3]),
873
              'proto': int(values[4]),
874
             }
875
    if values[5] is not None:
876
      retval['proto2'] = values[5]
877

    
878
    return retval
879

    
880
  @staticmethod
881
  def _DevPath(minor):
882
    """Return the path to a drbd device for a given minor.
883

884
    """
885
    return "/dev/drbd%d" % minor
886

    
887
  @classmethod
888
  def GetUsedDevs(cls):
889
    """Compute the list of used DRBD devices.
890

891
    """
892
    data = cls._GetProcData()
893

    
894
    used_devs = {}
895
    valid_line = re.compile("^ *([0-9]+): cs:([^ ]+).*$")
896
    for line in data:
897
      match = valid_line.match(line)
898
      if not match:
899
        continue
900
      minor = int(match.group(1))
901
      state = match.group(2)
902
      if state == cls._ST_UNCONFIGURED:
903
        continue
904
      used_devs[minor] = state, line
905

    
906
    return used_devs
907

    
908
  def _SetFromMinor(self, minor):
909
    """Set our parameters based on the given minor.
910

911
    This sets our minor variable and our dev_path.
912

913
    """
914
    if minor is None:
915
      self.minor = self.dev_path = None
916
      self.attached = False
917
    else:
918
      self.minor = minor
919
      self.dev_path = self._DevPath(minor)
920
      self.attached = True
921

    
922
  @staticmethod
923
  def _CheckMetaSize(meta_device):
924
    """Check if the given meta device looks like a valid one.
925

926
    This currently only check the size, which must be around
927
    128MiB.
928

929
    """
930
    result = utils.RunCmd(["blockdev", "--getsize", meta_device])
931
    if result.failed:
932
      _ThrowError("Failed to get device size: %s - %s",
933
                  result.fail_reason, result.output)
934
    try:
935
      sectors = int(result.stdout)
936
    except (TypeError, ValueError):
937
      _ThrowError("Invalid output from blockdev: '%s'", result.stdout)
938
    bytes = sectors * 512
939
    if bytes < 128 * 1024 * 1024: # less than 128MiB
940
      _ThrowError("Meta device too small (%.2fMib)", (bytes / 1024 / 1024))
941
    # the maximum *valid* size of the meta device when living on top
942
    # of LVM is hard to compute: it depends on the number of stripes
943
    # and the PE size; e.g. a 2-stripe, 64MB PE will result in a 128MB
944
    # (normal size), but an eight-stripe 128MB PE will result in a 1GB
945
    # size meta device; as such, we restrict it to 1GB (a little bit
946
    # too generous, but making assumptions about PE size is hard)
947
    if bytes > 1024 * 1024 * 1024:
948
      _ThrowError("Meta device too big (%.2fMiB)", (bytes / 1024 / 1024))
949

    
950
  def Rename(self, new_id):
951
    """Rename a device.
952

953
    This is not supported for drbd devices.
954

955
    """
956
    raise errors.ProgrammerError("Can't rename a drbd device")
957

    
958

    
959
class DRBD8(BaseDRBD):
960
  """DRBD v8.x block device.
961

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

966
  The unique_id for the drbd device is the (local_ip, local_port,
967
  remote_ip, remote_port) tuple, and it must have two children: the
968
  data device and the meta_device. The meta device is checked for
969
  valid size and is zeroed on create.
970

971
  """
972
  _MAX_MINORS = 255
973
  _PARSE_SHOW = None
974

    
975
  # timeout constants
976
  _NET_RECONFIG_TIMEOUT = 60
977

    
978
  def __init__(self, unique_id, children, size):
979
    if children and children.count(None) > 0:
980
      children = []
981
    if len(children) not in (0, 2):
982
      raise ValueError("Invalid configuration data %s" % str(children))
983
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 6:
984
      raise ValueError("Invalid configuration data %s" % str(unique_id))
985
    (self._lhost, self._lport,
986
     self._rhost, self._rport,
987
     self._aminor, self._secret) = unique_id
988
    if children:
989
      if not _CanReadDevice(children[1].dev_path):
990
        logging.info("drbd%s: Ignoring unreadable meta device", self._aminor)
991
        children = []
992
    super(DRBD8, self).__init__(unique_id, children, size)
993
    self.major = self._DRBD_MAJOR
994
    version = self._GetVersion()
995
    if version['k_major'] != 8 :
996
      _ThrowError("Mismatch in DRBD kernel version and requested ganeti"
997
                  " usage: kernel is %s.%s, ganeti wants 8.x",
998
                  version['k_major'], version['k_minor'])
999

    
1000
    if (self._lhost is not None and self._lhost == self._rhost and
1001
        self._lport == self._rport):
1002
      raise ValueError("Invalid configuration data, same local/remote %s" %
1003
                       (unique_id,))
1004
    self.Attach()
1005

    
1006
  @classmethod
1007
  def _InitMeta(cls, minor, dev_path):
1008
    """Initialize a meta device.
1009

1010
    This will not work if the given minor is in use.
1011

1012
    """
1013
    result = utils.RunCmd(["drbdmeta", "--force", cls._DevPath(minor),
1014
                           "v08", dev_path, "0", "create-md"])
1015
    if result.failed:
1016
      _ThrowError("Can't initialize meta device: %s", result.output)
1017

    
1018
  @classmethod
1019
  def _FindUnusedMinor(cls):
1020
    """Find an unused DRBD device.
1021

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

1025
    """
1026
    data = cls._GetProcData()
1027

    
1028
    unused_line = re.compile("^ *([0-9]+): cs:Unconfigured$")
1029
    used_line = re.compile("^ *([0-9]+): cs:")
1030
    highest = None
1031
    for line in data:
1032
      match = unused_line.match(line)
1033
      if match:
1034
        return int(match.group(1))
1035
      match = used_line.match(line)
1036
      if match:
1037
        minor = int(match.group(1))
1038
        highest = max(highest, minor)
1039
    if highest is None: # there are no minors in use at all
1040
      return 0
1041
    if highest >= cls._MAX_MINORS:
1042
      logging.error("Error: no free drbd minors!")
1043
      raise errors.BlockDeviceError("Can't find a free DRBD minor")
1044
    return highest + 1
1045

    
1046
  @classmethod
1047
  def _GetShowParser(cls):
1048
    """Return a parser for `drbd show` output.
1049

1050
    This will either create or return an already-create parser for the
1051
    output of the command `drbd show`.
1052

1053
    """
1054
    if cls._PARSE_SHOW is not None:
1055
      return cls._PARSE_SHOW
1056

    
1057
    # pyparsing setup
1058
    lbrace = pyp.Literal("{").suppress()
1059
    rbrace = pyp.Literal("}").suppress()
1060
    semi = pyp.Literal(";").suppress()
1061
    # this also converts the value to an int
1062
    number = pyp.Word(pyp.nums).setParseAction(lambda s, l, t: int(t[0]))
1063

    
1064
    comment = pyp.Literal ("#") + pyp.Optional(pyp.restOfLine)
1065
    defa = pyp.Literal("_is_default").suppress()
1066
    dbl_quote = pyp.Literal('"').suppress()
1067

    
1068
    keyword = pyp.Word(pyp.alphanums + '-')
1069

    
1070
    # value types
1071
    value = pyp.Word(pyp.alphanums + '_-/.:')
1072
    quoted = dbl_quote + pyp.CharsNotIn('"') + dbl_quote
1073
    addr_type = (pyp.Optional(pyp.Literal("ipv4")).suppress() +
1074
                 pyp.Optional(pyp.Literal("ipv6")).suppress())
1075
    addr_port = (addr_type + pyp.Word(pyp.nums + '.') +
1076
                 pyp.Literal(':').suppress() + number)
1077
    # meta device, extended syntax
1078
    meta_value = ((value ^ quoted) + pyp.Literal('[').suppress() +
1079
                  number + pyp.Word(']').suppress())
1080
    # device name, extended syntax
1081
    device_value = pyp.Literal("minor").suppress() + number
1082

    
1083
    # a statement
1084
    stmt = (~rbrace + keyword + ~lbrace +
1085
            pyp.Optional(addr_port ^ value ^ quoted ^ meta_value ^
1086
                         device_value) +
1087
            pyp.Optional(defa) + semi +
1088
            pyp.Optional(pyp.restOfLine).suppress())
1089

    
1090
    # an entire section
1091
    section_name = pyp.Word(pyp.alphas + '_')
1092
    section = section_name + lbrace + pyp.ZeroOrMore(pyp.Group(stmt)) + rbrace
1093

    
1094
    bnf = pyp.ZeroOrMore(pyp.Group(section ^ stmt))
1095
    bnf.ignore(comment)
1096

    
1097
    cls._PARSE_SHOW = bnf
1098

    
1099
    return bnf
1100

    
1101
  @classmethod
1102
  def _GetShowData(cls, minor):
1103
    """Return the `drbdsetup show` data for a minor.
1104

1105
    """
1106
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "show"])
1107
    if result.failed:
1108
      logging.error("Can't display the drbd config: %s - %s",
1109
                    result.fail_reason, result.output)
1110
      return None
1111
    return result.stdout
1112

    
1113
  @classmethod
1114
  def _GetDevInfo(cls, out):
1115
    """Parse details about a given DRBD minor.
1116

1117
    This return, if available, the local backing device (as a path)
1118
    and the local and remote (ip, port) information from a string
1119
    containing the output of the `drbdsetup show` command as returned
1120
    by _GetShowData.
1121

1122
    """
1123
    data = {}
1124
    if not out:
1125
      return data
1126

    
1127
    bnf = cls._GetShowParser()
1128
    # run pyparse
1129

    
1130
    try:
1131
      results = bnf.parseString(out)
1132
    except pyp.ParseException, err:
1133
      _ThrowError("Can't parse drbdsetup show output: %s", str(err))
1134

    
1135
    # and massage the results into our desired format
1136
    for section in results:
1137
      sname = section[0]
1138
      if sname == "_this_host":
1139
        for lst in section[1:]:
1140
          if lst[0] == "disk":
1141
            data["local_dev"] = lst[1]
1142
          elif lst[0] == "meta-disk":
1143
            data["meta_dev"] = lst[1]
1144
            data["meta_index"] = lst[2]
1145
          elif lst[0] == "address":
1146
            data["local_addr"] = tuple(lst[1:])
1147
      elif sname == "_remote_host":
1148
        for lst in section[1:]:
1149
          if lst[0] == "address":
1150
            data["remote_addr"] = tuple(lst[1:])
1151
    return data
1152

    
1153
  def _MatchesLocal(self, info):
1154
    """Test if our local config matches with an existing device.
1155

1156
    The parameter should be as returned from `_GetDevInfo()`. This
1157
    method tests if our local backing device is the same as the one in
1158
    the info parameter, in effect testing if we look like the given
1159
    device.
1160

1161
    """
1162
    if self._children:
1163
      backend, meta = self._children
1164
    else:
1165
      backend = meta = None
1166

    
1167
    if backend is not None:
1168
      retval = ("local_dev" in info and info["local_dev"] == backend.dev_path)
1169
    else:
1170
      retval = ("local_dev" not in info)
1171

    
1172
    if meta is not None:
1173
      retval = retval and ("meta_dev" in info and
1174
                           info["meta_dev"] == meta.dev_path)
1175
      retval = retval and ("meta_index" in info and
1176
                           info["meta_index"] == 0)
1177
    else:
1178
      retval = retval and ("meta_dev" not in info and
1179
                           "meta_index" not in info)
1180
    return retval
1181

    
1182
  def _MatchesNet(self, info):
1183
    """Test if our network config matches with an existing device.
1184

1185
    The parameter should be as returned from `_GetDevInfo()`. This
1186
    method tests if our network configuration is the same as the one
1187
    in the info parameter, in effect testing if we look like the given
1188
    device.
1189

1190
    """
1191
    if (((self._lhost is None and not ("local_addr" in info)) and
1192
         (self._rhost is None and not ("remote_addr" in info)))):
1193
      return True
1194

    
1195
    if self._lhost is None:
1196
      return False
1197

    
1198
    if not ("local_addr" in info and
1199
            "remote_addr" in info):
1200
      return False
1201

    
1202
    retval = (info["local_addr"] == (self._lhost, self._lport))
1203
    retval = (retval and
1204
              info["remote_addr"] == (self._rhost, self._rport))
1205
    return retval
1206

    
1207
  @classmethod
1208
  def _AssembleLocal(cls, minor, backend, meta, size):
1209
    """Configure the local part of a DRBD device.
1210

1211
    """
1212
    args = ["drbdsetup", cls._DevPath(minor), "disk",
1213
            backend, meta, "0",
1214
            "-e", "detach",
1215
            "--create-device"]
1216
    if size:
1217
      args.extend(["-d", "%sm" % size])
1218
    if not constants.DRBD_BARRIERS: # disable barriers, if configured so
1219
      version = cls._GetVersion()
1220
      # various DRBD versions support different disk barrier options;
1221
      # what we aim here is to revert back to the 'drain' method of
1222
      # disk flushes and to disable metadata barriers, in effect going
1223
      # back to pre-8.0.7 behaviour
1224
      vmaj = version['k_major']
1225
      vmin = version['k_minor']
1226
      vrel = version['k_point']
1227
      assert vmaj == 8
1228
      if vmin == 0: # 8.0.x
1229
        if vrel >= 12:
1230
          args.extend(['-i', '-m'])
1231
      elif vmin == 2: # 8.2.x
1232
        if vrel >= 7:
1233
          args.extend(['-i', '-m'])
1234
      elif vmaj >= 3: # 8.3.x or newer
1235
        args.extend(['-i', '-a', 'm'])
1236
    result = utils.RunCmd(args)
1237
    if result.failed:
1238
      _ThrowError("drbd%d: can't attach local disk: %s", minor, result.output)
1239

    
1240
  @classmethod
1241
  def _AssembleNet(cls, minor, net_info, protocol,
1242
                   dual_pri=False, hmac=None, secret=None):
1243
    """Configure the network part of the device.
1244

1245
    """
1246
    lhost, lport, rhost, rport = net_info
1247
    if None in net_info:
1248
      # we don't want network connection and actually want to make
1249
      # sure its shutdown
1250
      cls._ShutdownNet(minor)
1251
      return
1252

    
1253
    # Workaround for a race condition. When DRBD is doing its dance to
1254
    # establish a connection with its peer, it also sends the
1255
    # synchronization speed over the wire. In some cases setting the
1256
    # sync speed only after setting up both sides can race with DRBD
1257
    # connecting, hence we set it here before telling DRBD anything
1258
    # about its peer.
1259
    cls._SetMinorSyncSpeed(minor, constants.SYNC_SPEED)
1260

    
1261
    args = ["drbdsetup", cls._DevPath(minor), "net",
1262
            "%s:%s" % (lhost, lport), "%s:%s" % (rhost, rport), protocol,
1263
            "-A", "discard-zero-changes",
1264
            "-B", "consensus",
1265
            "--create-device",
1266
            ]
1267
    if dual_pri:
1268
      args.append("-m")
1269
    if hmac and secret:
1270
      args.extend(["-a", hmac, "-x", secret])
1271
    result = utils.RunCmd(args)
1272
    if result.failed:
1273
      _ThrowError("drbd%d: can't setup network: %s - %s",
1274
                  minor, result.fail_reason, result.output)
1275

    
1276
    def _CheckNetworkConfig():
1277
      info = cls._GetDevInfo(cls._GetShowData(minor))
1278
      if not "local_addr" in info or not "remote_addr" in info:
1279
        raise utils.RetryAgain()
1280

    
1281
      if (info["local_addr"] != (lhost, lport) or
1282
          info["remote_addr"] != (rhost, rport)):
1283
        raise utils.RetryAgain()
1284

    
1285
    try:
1286
      utils.Retry(_CheckNetworkConfig, 1.0, 10.0)
1287
    except utils.RetryTimeout:
1288
      _ThrowError("drbd%d: timeout while configuring network", minor)
1289

    
1290
  def AddChildren(self, devices):
1291
    """Add a disk to the DRBD device.
1292

1293
    """
1294
    if self.minor is None:
1295
      _ThrowError("drbd%d: can't attach to dbrd8 during AddChildren",
1296
                  self._aminor)
1297
    if len(devices) != 2:
1298
      _ThrowError("drbd%d: need two devices for AddChildren", self.minor)
1299
    info = self._GetDevInfo(self._GetShowData(self.minor))
1300
    if "local_dev" in info:
1301
      _ThrowError("drbd%d: already attached to a local disk", self.minor)
1302
    backend, meta = devices
1303
    if backend.dev_path is None or meta.dev_path is None:
1304
      _ThrowError("drbd%d: children not ready during AddChildren", self.minor)
1305
    backend.Open()
1306
    meta.Open()
1307
    self._CheckMetaSize(meta.dev_path)
1308
    self._InitMeta(self._FindUnusedMinor(), meta.dev_path)
1309

    
1310
    self._AssembleLocal(self.minor, backend.dev_path, meta.dev_path, self.size)
1311
    self._children = devices
1312

    
1313
  def RemoveChildren(self, devices):
1314
    """Detach the drbd device from local storage.
1315

1316
    """
1317
    if self.minor is None:
1318
      _ThrowError("drbd%d: can't attach to drbd8 during RemoveChildren",
1319
                  self._aminor)
1320
    # early return if we don't actually have backing storage
1321
    info = self._GetDevInfo(self._GetShowData(self.minor))
1322
    if "local_dev" not in info:
1323
      return
1324
    if len(self._children) != 2:
1325
      _ThrowError("drbd%d: we don't have two children: %s", self.minor,
1326
                  self._children)
1327
    if self._children.count(None) == 2: # we don't actually have children :)
1328
      logging.warning("drbd%d: requested detach while detached", self.minor)
1329
      return
1330
    if len(devices) != 2:
1331
      _ThrowError("drbd%d: we need two children in RemoveChildren", self.minor)
1332
    for child, dev in zip(self._children, devices):
1333
      if dev != child.dev_path:
1334
        _ThrowError("drbd%d: mismatch in local storage (%s != %s) in"
1335
                    " RemoveChildren", self.minor, dev, child.dev_path)
1336

    
1337
    self._ShutdownLocal(self.minor)
1338
    self._children = []
1339

    
1340
  @classmethod
1341
  def _SetMinorSyncSpeed(cls, minor, kbytes):
1342
    """Set the speed of the DRBD syncer.
1343

1344
    This is the low-level implementation.
1345

1346
    @type minor: int
1347
    @param minor: the drbd minor whose settings we change
1348
    @type kbytes: int
1349
    @param kbytes: the speed in kbytes/second
1350
    @rtype: boolean
1351
    @return: the success of the operation
1352

1353
    """
1354
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "syncer",
1355
                           "-r", "%d" % kbytes, "--create-device"])
1356
    if result.failed:
1357
      logging.error("Can't change syncer rate: %s - %s",
1358
                    result.fail_reason, result.output)
1359
    return not result.failed
1360

    
1361
  def SetSyncSpeed(self, kbytes):
1362
    """Set the speed of the DRBD syncer.
1363

1364
    @type kbytes: int
1365
    @param kbytes: the speed in kbytes/second
1366
    @rtype: boolean
1367
    @return: the success of the operation
1368

1369
    """
1370
    if self.minor is None:
1371
      logging.info("Not attached during SetSyncSpeed")
1372
      return False
1373
    children_result = super(DRBD8, self).SetSyncSpeed(kbytes)
1374
    return self._SetMinorSyncSpeed(self.minor, kbytes) and children_result
1375

    
1376
  def GetProcStatus(self):
1377
    """Return device data from /proc.
1378

1379
    """
1380
    if self.minor is None:
1381
      _ThrowError("drbd%d: GetStats() called while not attached", self._aminor)
1382
    proc_info = self._MassageProcData(self._GetProcData())
1383
    if self.minor not in proc_info:
1384
      _ThrowError("drbd%d: can't find myself in /proc", self.minor)
1385
    return DRBD8Status(proc_info[self.minor])
1386

    
1387
  def GetSyncStatus(self):
1388
    """Returns the sync status of the device.
1389

1390

1391
    If sync_percent is None, it means all is ok
1392
    If estimated_time is None, it means we can't estimate
1393
    the time needed, otherwise it's the time left in seconds.
1394

1395

1396
    We set the is_degraded parameter to True on two conditions:
1397
    network not connected or local disk missing.
1398

1399
    We compute the ldisk parameter based on whether we have a local
1400
    disk or not.
1401

1402
    @rtype: objects.BlockDevStatus
1403

1404
    """
1405
    if self.minor is None and not self.Attach():
1406
      _ThrowError("drbd%d: can't Attach() in GetSyncStatus", self._aminor)
1407

    
1408
    stats = self.GetProcStatus()
1409
    is_degraded = not stats.is_connected or not stats.is_disk_uptodate
1410

    
1411
    if stats.is_disk_uptodate:
1412
      ldisk_status = constants.LDS_OKAY
1413
    elif stats.is_diskless:
1414
      ldisk_status = constants.LDS_FAULTY
1415
    else:
1416
      ldisk_status = constants.LDS_UNKNOWN
1417

    
1418
    return objects.BlockDevStatus(dev_path=self.dev_path,
1419
                                  major=self.major,
1420
                                  minor=self.minor,
1421
                                  sync_percent=stats.sync_percent,
1422
                                  estimated_time=stats.est_time,
1423
                                  is_degraded=is_degraded,
1424
                                  ldisk_status=ldisk_status)
1425

    
1426
  def Open(self, force=False):
1427
    """Make the local state primary.
1428

1429
    If the 'force' parameter is given, the '-o' option is passed to
1430
    drbdsetup. Since this is a potentially dangerous operation, the
1431
    force flag should be only given after creation, when it actually
1432
    is mandatory.
1433

1434
    """
1435
    if self.minor is None and not self.Attach():
1436
      logging.error("DRBD cannot attach to a device during open")
1437
      return False
1438
    cmd = ["drbdsetup", self.dev_path, "primary"]
1439
    if force:
1440
      cmd.append("-o")
1441
    result = utils.RunCmd(cmd)
1442
    if result.failed:
1443
      _ThrowError("drbd%d: can't make drbd device primary: %s", self.minor,
1444
                  result.output)
1445

    
1446
  def Close(self):
1447
    """Make the local state secondary.
1448

1449
    This will, of course, fail if the device is in use.
1450

1451
    """
1452
    if self.minor is None and not self.Attach():
1453
      _ThrowError("drbd%d: can't Attach() in Close()", self._aminor)
1454
    result = utils.RunCmd(["drbdsetup", self.dev_path, "secondary"])
1455
    if result.failed:
1456
      _ThrowError("drbd%d: can't switch drbd device to secondary: %s",
1457
                  self.minor, result.output)
1458

    
1459
  def DisconnectNet(self):
1460
    """Removes network configuration.
1461

1462
    This method shutdowns the network side of the device.
1463

1464
    The method will wait up to a hardcoded timeout for the device to
1465
    go into standalone after the 'disconnect' command before
1466
    re-configuring it, as sometimes it takes a while for the
1467
    disconnect to actually propagate and thus we might issue a 'net'
1468
    command while the device is still connected. If the device will
1469
    still be attached to the network and we time out, we raise an
1470
    exception.
1471

1472
    """
1473
    if self.minor is None:
1474
      _ThrowError("drbd%d: disk not attached in re-attach net", self._aminor)
1475

    
1476
    if None in (self._lhost, self._lport, self._rhost, self._rport):
1477
      _ThrowError("drbd%d: DRBD disk missing network info in"
1478
                  " DisconnectNet()", self.minor)
1479

    
1480
    class _DisconnectStatus:
1481
      def __init__(self, ever_disconnected):
1482
        self.ever_disconnected = ever_disconnected
1483

    
1484
    dstatus = _DisconnectStatus(_IgnoreError(self._ShutdownNet, self.minor))
1485

    
1486
    def _WaitForDisconnect():
1487
      if self.GetProcStatus().is_standalone:
1488
        return
1489

    
1490
      # retry the disconnect, it seems possible that due to a well-time
1491
      # disconnect on the peer, my disconnect command might be ignored and
1492
      # forgotten
1493
      dstatus.ever_disconnected = \
1494
        _IgnoreError(self._ShutdownNet, self.minor) or dstatus.ever_disconnected
1495

    
1496
      raise utils.RetryAgain()
1497

    
1498
    # Keep start time
1499
    start_time = time.time()
1500

    
1501
    try:
1502
      # Start delay at 100 milliseconds and grow up to 2 seconds
1503
      utils.Retry(_WaitForDisconnect, (0.1, 1.5, 2.0),
1504
                  self._NET_RECONFIG_TIMEOUT)
1505
    except utils.RetryTimeout:
1506
      if dstatus.ever_disconnected:
1507
        msg = ("drbd%d: device did not react to the"
1508
               " 'disconnect' command in a timely manner")
1509
      else:
1510
        msg = "drbd%d: can't shutdown network, even after multiple retries"
1511

    
1512
      _ThrowError(msg, self.minor)
1513

    
1514
    reconfig_time = time.time() - start_time
1515
    if reconfig_time > (self._NET_RECONFIG_TIMEOUT * 0.25):
1516
      logging.info("drbd%d: DisconnectNet: detach took %.3f seconds",
1517
                   self.minor, reconfig_time)
1518

    
1519
  def AttachNet(self, multimaster):
1520
    """Reconnects the network.
1521

1522
    This method connects the network side of the device with a
1523
    specified multi-master flag. The device needs to be 'Standalone'
1524
    but have valid network configuration data.
1525

1526
    Args:
1527
      - multimaster: init the network in dual-primary mode
1528

1529
    """
1530
    if self.minor is None:
1531
      _ThrowError("drbd%d: device not attached in AttachNet", self._aminor)
1532

    
1533
    if None in (self._lhost, self._lport, self._rhost, self._rport):
1534
      _ThrowError("drbd%d: missing network info in AttachNet()", self.minor)
1535

    
1536
    status = self.GetProcStatus()
1537

    
1538
    if not status.is_standalone:
1539
      _ThrowError("drbd%d: device is not standalone in AttachNet", self.minor)
1540

    
1541
    self._AssembleNet(self.minor,
1542
                      (self._lhost, self._lport, self._rhost, self._rport),
1543
                      constants.DRBD_NET_PROTOCOL, dual_pri=multimaster,
1544
                      hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1545

    
1546
  def Attach(self):
1547
    """Check if our minor is configured.
1548

1549
    This doesn't do any device configurations - it only checks if the
1550
    minor is in a state different from Unconfigured.
1551

1552
    Note that this function will not change the state of the system in
1553
    any way (except in case of side-effects caused by reading from
1554
    /proc).
1555

1556
    """
1557
    used_devs = self.GetUsedDevs()
1558
    if self._aminor in used_devs:
1559
      minor = self._aminor
1560
    else:
1561
      minor = None
1562

    
1563
    self._SetFromMinor(minor)
1564
    return minor is not None
1565

    
1566
  def Assemble(self):
1567
    """Assemble the drbd.
1568

1569
    Method:
1570
      - if we have a configured device, we try to ensure that it matches
1571
        our config
1572
      - if not, we create it from zero
1573

1574
    """
1575
    super(DRBD8, self).Assemble()
1576

    
1577
    self.Attach()
1578
    if self.minor is None:
1579
      # local device completely unconfigured
1580
      self._FastAssemble()
1581
    else:
1582
      # we have to recheck the local and network status and try to fix
1583
      # the device
1584
      self._SlowAssemble()
1585

    
1586
  def _SlowAssemble(self):
1587
    """Assembles the DRBD device from a (partially) configured device.
1588

1589
    In case of partially attached (local device matches but no network
1590
    setup), we perform the network attach. If successful, we re-test
1591
    the attach if can return success.
1592

1593
    """
1594
    # TODO: Rewrite to not use a for loop just because there is 'break'
1595
    # pylint: disable-msg=W0631
1596
    net_data = (self._lhost, self._lport, self._rhost, self._rport)
1597
    for minor in (self._aminor,):
1598
      info = self._GetDevInfo(self._GetShowData(minor))
1599
      match_l = self._MatchesLocal(info)
1600
      match_r = self._MatchesNet(info)
1601

    
1602
      if match_l and match_r:
1603
        # everything matches
1604
        break
1605

    
1606
      if match_l and not match_r and "local_addr" not in info:
1607
        # disk matches, but not attached to network, attach and recheck
1608
        self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
1609
                          hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1610
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1611
          break
1612
        else:
1613
          _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
1614
                      " show' disagrees", minor)
1615

    
1616
      if match_r and "local_dev" not in info:
1617
        # no local disk, but network attached and it matches
1618
        self._AssembleLocal(minor, self._children[0].dev_path,
1619
                            self._children[1].dev_path, self.size)
1620
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1621
          break
1622
        else:
1623
          _ThrowError("drbd%d: disk attach successful, but 'drbdsetup"
1624
                      " show' disagrees", minor)
1625

    
1626
      # this case must be considered only if we actually have local
1627
      # storage, i.e. not in diskless mode, because all diskless
1628
      # devices are equal from the point of view of local
1629
      # configuration
1630
      if (match_l and "local_dev" in info and
1631
          not match_r and "local_addr" in info):
1632
        # strange case - the device network part points to somewhere
1633
        # else, even though its local storage is ours; as we own the
1634
        # drbd space, we try to disconnect from the remote peer and
1635
        # reconnect to our correct one
1636
        try:
1637
          self._ShutdownNet(minor)
1638
        except errors.BlockDeviceError, err:
1639
          _ThrowError("drbd%d: device has correct local storage, wrong"
1640
                      " remote peer and is unable to disconnect in order"
1641
                      " to attach to the correct peer: %s", minor, str(err))
1642
        # note: _AssembleNet also handles the case when we don't want
1643
        # local storage (i.e. one or more of the _[lr](host|port) is
1644
        # None)
1645
        self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
1646
                          hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1647
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1648
          break
1649
        else:
1650
          _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
1651
                      " show' disagrees", minor)
1652

    
1653
    else:
1654
      minor = None
1655

    
1656
    self._SetFromMinor(minor)
1657
    if minor is None:
1658
      _ThrowError("drbd%d: cannot activate, unknown or unhandled reason",
1659
                  self._aminor)
1660

    
1661
  def _FastAssemble(self):
1662
    """Assemble the drbd device from zero.
1663

1664
    This is run when in Assemble we detect our minor is unused.
1665

1666
    """
1667
    minor = self._aminor
1668
    if self._children and self._children[0] and self._children[1]:
1669
      self._AssembleLocal(minor, self._children[0].dev_path,
1670
                          self._children[1].dev_path, self.size)
1671
    if self._lhost and self._lport and self._rhost and self._rport:
1672
      self._AssembleNet(minor,
1673
                        (self._lhost, self._lport, self._rhost, self._rport),
1674
                        constants.DRBD_NET_PROTOCOL,
1675
                        hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1676
    self._SetFromMinor(minor)
1677

    
1678
  @classmethod
1679
  def _ShutdownLocal(cls, minor):
1680
    """Detach from the local device.
1681

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

1685
    """
1686
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "detach"])
1687
    if result.failed:
1688
      _ThrowError("drbd%d: can't detach local disk: %s", minor, result.output)
1689

    
1690
  @classmethod
1691
  def _ShutdownNet(cls, minor):
1692
    """Disconnect from the remote peer.
1693

1694
    This fails if we don't have a local device.
1695

1696
    """
1697
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disconnect"])
1698
    if result.failed:
1699
      _ThrowError("drbd%d: can't shutdown network: %s", minor, result.output)
1700

    
1701
  @classmethod
1702
  def _ShutdownAll(cls, minor):
1703
    """Deactivate the device.
1704

1705
    This will, of course, fail if the device is in use.
1706

1707
    """
1708
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "down"])
1709
    if result.failed:
1710
      _ThrowError("drbd%d: can't shutdown drbd device: %s",
1711
                  minor, result.output)
1712

    
1713
  def Shutdown(self):
1714
    """Shutdown the DRBD device.
1715

1716
    """
1717
    if self.minor is None and not self.Attach():
1718
      logging.info("drbd%d: not attached during Shutdown()", self._aminor)
1719
      return
1720
    minor = self.minor
1721
    self.minor = None
1722
    self.dev_path = None
1723
    self._ShutdownAll(minor)
1724

    
1725
  def Remove(self):
1726
    """Stub remove for DRBD devices.
1727

1728
    """
1729
    self.Shutdown()
1730

    
1731
  @classmethod
1732
  def Create(cls, unique_id, children, size):
1733
    """Create a new DRBD8 device.
1734

1735
    Since DRBD devices are not created per se, just assembled, this
1736
    function only initializes the metadata.
1737

1738
    """
1739
    if len(children) != 2:
1740
      raise errors.ProgrammerError("Invalid setup for the drbd device")
1741
    # check that the minor is unused
1742
    aminor = unique_id[4]
1743
    proc_info = cls._MassageProcData(cls._GetProcData())
1744
    if aminor in proc_info:
1745
      status = DRBD8Status(proc_info[aminor])
1746
      in_use = status.is_in_use
1747
    else:
1748
      in_use = False
1749
    if in_use:
1750
      _ThrowError("drbd%d: minor is already in use at Create() time", aminor)
1751
    meta = children[1]
1752
    meta.Assemble()
1753
    if not meta.Attach():
1754
      _ThrowError("drbd%d: can't attach to meta device '%s'",
1755
                  aminor, meta)
1756
    cls._CheckMetaSize(meta.dev_path)
1757
    cls._InitMeta(aminor, meta.dev_path)
1758
    return cls(unique_id, children, size)
1759

    
1760
  def Grow(self, amount):
1761
    """Resize the DRBD device and its backing storage.
1762

1763
    """
1764
    if self.minor is None:
1765
      _ThrowError("drbd%d: Grow called while not attached", self._aminor)
1766
    if len(self._children) != 2 or None in self._children:
1767
      _ThrowError("drbd%d: cannot grow diskless device", self.minor)
1768
    self._children[0].Grow(amount)
1769
    result = utils.RunCmd(["drbdsetup", self.dev_path, "resize", "-s",
1770
                           "%dm" % (self.size + amount)])
1771
    if result.failed:
1772
      _ThrowError("drbd%d: resize failed: %s", self.minor, result.output)
1773

    
1774

    
1775
class FileStorage(BlockDev):
1776
  """File device.
1777

1778
  This class represents the a file storage backend device.
1779

1780
  The unique_id for the file device is a (file_driver, file_path) tuple.
1781

1782
  """
1783
  def __init__(self, unique_id, children, size):
1784
    """Initalizes a file device backend.
1785

1786
    """
1787
    if children:
1788
      raise errors.BlockDeviceError("Invalid setup for file device")
1789
    super(FileStorage, self).__init__(unique_id, children, size)
1790
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1791
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1792
    self.driver = unique_id[0]
1793
    self.dev_path = unique_id[1]
1794
    self.Attach()
1795

    
1796
  def Assemble(self):
1797
    """Assemble the device.
1798

1799
    Checks whether the file device exists, raises BlockDeviceError otherwise.
1800

1801
    """
1802
    if not os.path.exists(self.dev_path):
1803
      _ThrowError("File device '%s' does not exist" % self.dev_path)
1804

    
1805
  def Shutdown(self):
1806
    """Shutdown the device.
1807

1808
    This is a no-op for the file type, as we don't deactivate
1809
    the file on shutdown.
1810

1811
    """
1812
    pass
1813

    
1814
  def Open(self, force=False):
1815
    """Make the device ready for I/O.
1816

1817
    This is a no-op for the file type.
1818

1819
    """
1820
    pass
1821

    
1822
  def Close(self):
1823
    """Notifies that the device will no longer be used for I/O.
1824

1825
    This is a no-op for the file type.
1826

1827
    """
1828
    pass
1829

    
1830
  def Remove(self):
1831
    """Remove the file backing the block device.
1832

1833
    @rtype: boolean
1834
    @return: True if the removal was successful
1835

1836
    """
1837
    try:
1838
      os.remove(self.dev_path)
1839
    except OSError, err:
1840
      if err.errno != errno.ENOENT:
1841
        _ThrowError("Can't remove file '%s': %s", self.dev_path, err)
1842

    
1843
  def Rename(self, new_id):
1844
    """Renames the file.
1845

1846
    """
1847
    # TODO: implement rename for file-based storage
1848
    _ThrowError("Rename is not supported for file-based storage")
1849

    
1850
  def Grow(self, amount):
1851
    """Grow the file
1852

1853
    @param amount: the amount (in mebibytes) to grow with
1854

1855
    """
1856
    # TODO: implement grow for file-based storage
1857
    _ThrowError("Grow not supported for file-based storage")
1858

    
1859
  def Attach(self):
1860
    """Attach to an existing file.
1861

1862
    Check if this file already exists.
1863

1864
    @rtype: boolean
1865
    @return: True if file exists
1866

1867
    """
1868
    self.attached = os.path.exists(self.dev_path)
1869
    return self.attached
1870

    
1871
  def GetActualSize(self):
1872
    """Return the actual disk size.
1873

1874
    @note: the device needs to be active when this is called
1875

1876
    """
1877
    assert self.attached, "BlockDevice not attached in GetActualSize()"
1878
    try:
1879
      st = os.stat(self.dev_path)
1880
      return st.st_size
1881
    except OSError, err:
1882
      _ThrowError("Can't stat %s: %s", self.dev_path, err)
1883

    
1884
  @classmethod
1885
  def Create(cls, unique_id, children, size):
1886
    """Create a new file.
1887

1888
    @param size: the size of file in MiB
1889

1890
    @rtype: L{bdev.FileStorage}
1891
    @return: an instance of FileStorage
1892

1893
    """
1894
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1895
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1896
    dev_path = unique_id[1]
1897
    if os.path.exists(dev_path):
1898
      _ThrowError("File already existing: %s", dev_path)
1899
    try:
1900
      f = open(dev_path, 'w')
1901
      f.truncate(size * 1024 * 1024)
1902
      f.close()
1903
    except IOError, err:
1904
      _ThrowError("Error in file creation: %", str(err))
1905

    
1906
    return FileStorage(unique_id, children, size)
1907

    
1908

    
1909
DEV_MAP = {
1910
  constants.LD_LV: LogicalVolume,
1911
  constants.LD_DRBD8: DRBD8,
1912
  constants.LD_FILE: FileStorage,
1913
  }
1914

    
1915

    
1916
def FindDevice(dev_type, unique_id, children, size):
1917
  """Search for an existing, assembled device.
1918

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

1922
  """
1923
  if dev_type not in DEV_MAP:
1924
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1925
  device = DEV_MAP[dev_type](unique_id, children, size)
1926
  if not device.attached:
1927
    return None
1928
  return device
1929

    
1930

    
1931
def Assemble(dev_type, unique_id, children, size):
1932
  """Try to attach or assemble an existing device.
1933

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

1937
  """
1938
  if dev_type not in DEV_MAP:
1939
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1940
  device = DEV_MAP[dev_type](unique_id, children, size)
1941
  device.Assemble()
1942
  return device
1943

    
1944

    
1945
def Create(dev_type, unique_id, children, size):
1946
  """Create a device.
1947

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