Statistics
| Branch: | Tag: | Revision:

root / lib / bdev.py @ 099c52ad

History | View | Annotate | Download (60.4 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
def _IgnoreError(fn, *args, **kwargs):
38
  """Executes the given function, ignoring BlockDeviceErrors.
39

40
  This is used in order to simplify the execution of cleanup or
41
  rollback functions.
42

43
  @rtype: boolean
44
  @return: True when fn didn't raise an exception, False otherwise
45

46
  """
47
  try:
48
    fn(*args, **kwargs)
49
    return True
50
  except errors.BlockDeviceError, err:
51
    logging.warning("Caught BlockDeviceError but ignoring: %s", str(err))
52
    return False
53

    
54

    
55
def _ThrowError(msg, *args):
56
  """Log an error to the node daemon and the raise an exception.
57

58
  @type msg: string
59
  @param msg: the text of the exception
60
  @raise errors.BlockDeviceError
61

62
  """
63
  if args:
64
    msg = msg % args
65
  logging.error(msg)
66
  raise errors.BlockDeviceError(msg)
67

    
68

    
69
class BlockDev(object):
70
  """Block device abstract class.
71

72
  A block device can be in the following states:
73
    - not existing on the system, and by `Create()` it goes into:
74
    - existing but not setup/not active, and by `Assemble()` goes into:
75
    - active read-write and by `Open()` it goes into
76
    - online (=used, or ready for use)
77

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

84
  The many different states of the device are due to the fact that we
85
  need to cover many device types:
86
    - logical volumes are created, lvchange -a y $lv, and used
87
    - drbd devices are attached to a local disk/remote peer and made primary
88

89
  A block device is identified by three items:
90
    - the /dev path of the device (dynamic)
91
    - a unique ID of the device (static)
92
    - it's major/minor pair (dynamic)
93

94
  Not all devices implement both the first two as distinct items. LVM
95
  logical volumes have their unique ID (the pair volume group, logical
96
  volume name) in a 1-to-1 relation to the dev path. For DRBD devices,
97
  the /dev path is again dynamic and the unique id is the pair (host1,
98
  dev1), (host2, dev2).
99

100
  You can get to a device in two ways:
101
    - creating the (real) device, which returns you
102
      an attached instance (lvcreate)
103
    - attaching of a python instance to an existing (real) device
104

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

111
  """
112
  def __init__(self, unique_id, children, size):
113
    self._children = children
114
    self.dev_path = None
115
    self.unique_id = unique_id
116
    self.major = None
117
    self.minor = None
118
    self.attached = False
119
    self.size = size
120

    
121
  def Assemble(self):
122
    """Assemble the device from its components.
123

124
    Implementations of this method by child classes must ensure that:
125
      - after the device has been assembled, it knows its major/minor
126
        numbers; this allows other devices (usually parents) to probe
127
        correctly for their children
128
      - calling this method on an existing, in-use device is safe
129
      - if the device is already configured (and in an OK state),
130
        this method is idempotent
131

132
    """
133
    pass
134

    
135
  def Attach(self):
136
    """Find a device which matches our config and attach to it.
137

138
    """
139
    raise NotImplementedError
140

    
141
  def Close(self):
142
    """Notifies that the device will no longer be used for I/O.
143

144
    """
145
    raise NotImplementedError
146

    
147
  @classmethod
148
  def Create(cls, unique_id, children, size):
149
    """Create the device.
150

151
    If the device cannot be created, it will return None
152
    instead. Error messages go to the logging system.
153

154
    Note that for some devices, the unique_id is used, and for other,
155
    the children. The idea is that these two, taken together, are
156
    enough for both creation and assembly (later).
157

158
    """
159
    raise NotImplementedError
160

    
161
  def Remove(self):
162
    """Remove this device.
163

164
    This makes sense only for some of the device types: LV and file
165
    storage. Also note that if the device can't attach, the removal
166
    can't be completed.
167

168
    """
169
    raise NotImplementedError
170

    
171
  def Rename(self, new_id):
172
    """Rename this device.
173

174
    This may or may not make sense for a given device type.
175

176
    """
177
    raise NotImplementedError
178

    
179
  def Open(self, force=False):
180
    """Make the device ready for use.
181

182
    This makes the device ready for I/O. For now, just the DRBD
183
    devices need this.
184

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

188
    """
189
    raise NotImplementedError
190

    
191
  def Shutdown(self):
192
    """Shut down the device, freeing its children.
193

194
    This undoes the `Assemble()` work, except for the child
195
    assembling; as such, the children on the device are still
196
    assembled after this call.
197

198
    """
199
    raise NotImplementedError
200

    
201
  def SetSyncSpeed(self, speed):
202
    """Adjust the sync speed of the mirror.
203

204
    In case this is not a mirroring device, this is no-op.
205

206
    """
207
    result = True
208
    if self._children:
209
      for child in self._children:
210
        result = result and child.SetSyncSpeed(speed)
211
    return result
212

    
213
  def GetSyncStatus(self):
214
    """Returns the sync status of the device.
215

216
    If this device is a mirroring device, this function returns the
217
    status of the mirror.
218

219
    If sync_percent is None, it means the device is not syncing.
220

221
    If estimated_time is None, it means we can't estimate
222
    the time needed, otherwise it's the time left in seconds.
223

224
    If is_degraded is True, it means the device is missing
225
    redundancy. This is usually a sign that something went wrong in
226
    the device setup, if sync_percent is None.
227

228
    The ldisk parameter represents the degradation of the local
229
    data. This is only valid for some devices, the rest will always
230
    return False (not degraded).
231

232
    @rtype: objects.BlockDevStatus
233

234
    """
235
    return objects.BlockDevStatus(dev_path=self.dev_path,
236
                                  major=self.major,
237
                                  minor=self.minor,
238
                                  sync_percent=None,
239
                                  estimated_time=None,
240
                                  is_degraded=False,
241
                                  ldisk_status=constants.LDS_OKAY)
242

    
243
  def CombinedSyncStatus(self):
244
    """Calculate the mirror status recursively for our children.
245

246
    The return value is the same as for `GetSyncStatus()` except the
247
    minimum percent and maximum time are calculated across our
248
    children.
249

250
    @rtype: objects.BlockDevStatus
251

252
    """
253
    status = self.GetSyncStatus()
254

    
255
    min_percent = status.sync_percent
256
    max_time = status.estimated_time
257
    is_degraded = status.is_degraded
258
    ldisk_status = status.ldisk_status
259

    
260
    if self._children:
261
      for child in self._children:
262
        child_status = child.GetSyncStatus()
263

    
264
        if min_percent is None:
265
          min_percent = child_status.sync_percent
266
        elif child_status.sync_percent is not None:
267
          min_percent = min(min_percent, child_status.sync_percent)
268

    
269
        if max_time is None:
270
          max_time = child_status.estimated_time
271
        elif child_status.estimated_time is not None:
272
          max_time = max(max_time, child_status.estimated_time)
273

    
274
        is_degraded = is_degraded or child_status.is_degraded
275

    
276
        if ldisk_status is None:
277
          ldisk_status = child_status.ldisk_status
278
        elif child_status.ldisk_status is not None:
279
          ldisk_status = max(ldisk_status, child_status.ldisk_status)
280

    
281
    return objects.BlockDevStatus(dev_path=self.dev_path,
282
                                  major=self.major,
283
                                  minor=self.minor,
284
                                  sync_percent=min_percent,
285
                                  estimated_time=max_time,
286
                                  is_degraded=is_degraded,
287
                                  ldisk_status=ldisk_status)
288

    
289

    
290
  def SetInfo(self, text):
291
    """Update metadata with info text.
292

293
    Only supported for some device types.
294

295
    """
296
    for child in self._children:
297
      child.SetInfo(text)
298

    
299
  def Grow(self, amount):
300
    """Grow the block device.
301

302
    @param amount: the amount (in mebibytes) to grow with
303

304
    """
305
    raise NotImplementedError
306

    
307
  def GetActualSize(self):
308
    """Return the actual disk size.
309

310
    @note: the device needs to be active when this is called
311

312
    """
313
    assert self.attached, "BlockDevice not attached in GetActualSize()"
314
    result = utils.RunCmd(["blockdev", "--getsize64", self.dev_path])
315
    if result.failed:
316
      _ThrowError("blockdev failed (%s): %s",
317
                  result.fail_reason, result.output)
318
    try:
319
      sz = int(result.output.strip())
320
    except (ValueError, TypeError), err:
321
      _ThrowError("Failed to parse blockdev output: %s", str(err))
322
    return sz
323

    
324
  def __repr__(self):
325
    return ("<%s: unique_id: %s, children: %s, %s:%s, %s>" %
326
            (self.__class__, self.unique_id, self._children,
327
             self.major, self.minor, self.dev_path))
328

    
329

    
330
class LogicalVolume(BlockDev):
331
  """Logical Volume block device.
332

333
  """
334
  def __init__(self, unique_id, children, size):
335
    """Attaches to a LV device.
336

337
    The unique_id is a tuple (vg_name, lv_name)
338

339
    """
340
    super(LogicalVolume, self).__init__(unique_id, children, size)
341
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
342
      raise ValueError("Invalid configuration data %s" % str(unique_id))
343
    self._vg_name, self._lv_name = unique_id
344
    self.dev_path = "/dev/%s/%s" % (self._vg_name, self._lv_name)
345
    self._degraded = True
346
    self.major = self.minor = self.pe_size = self.stripe_count = None
347
    self.Attach()
348

    
349
  @classmethod
350
  def Create(cls, unique_id, children, size):
351
    """Create a new logical volume.
352

353
    """
354
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
355
      raise errors.ProgrammerError("Invalid configuration data %s" %
356
                                   str(unique_id))
357
    vg_name, lv_name = unique_id
358
    pvs_info = cls.GetPVInfo([vg_name])
359
    if not pvs_info:
360
      _ThrowError("Can't compute PV info for vg %s", vg_name)
361
    pvs_info.sort()
362
    pvs_info.reverse()
363

    
364
    pvlist = [ pv[1] for pv in pvs_info ]
365
    if utils.any(pvlist, lambda v: ":" in v):
366
      _ThrowError("Some of your PVs have invalid character ':'"
367
                  " in their name")
368
    free_size = sum([ pv[0] for pv in pvs_info ])
369
    current_pvs = len(pvlist)
370
    stripes = min(current_pvs, constants.LVM_STRIPECOUNT)
371

    
372
    # The size constraint should have been checked from the master before
373
    # calling the create function.
374
    if free_size < size:
375
      _ThrowError("Not enough free space: required %s,"
376
                  " available %s", size, free_size)
377
    cmd = ["lvcreate", "-L%dm" % size, "-n%s" % lv_name]
378
    # If the free space is not well distributed, we won't be able to
379
    # create an optimally-striped volume; in that case, we want to try
380
    # with N, N-1, ..., 2, and finally 1 (non-stripped) number of
381
    # stripes
382
    for stripes_arg in range(stripes, 0, -1):
383
      result = utils.RunCmd(cmd + ["-i%d" % stripes_arg] + [vg_name] + pvlist)
384
      if not result.failed:
385
        break
386
    if result.failed:
387
      _ThrowError("LV create failed (%s): %s",
388
                  result.fail_reason, result.output)
389
    return LogicalVolume(unique_id, children, size)
390

    
391
  @staticmethod
392
  def GetPVInfo(vg_names, filter_allocatable=True):
393
    """Get the free space info for PVs in a volume group.
394

395
    @param vg_names: list of volume group names, if empty all will be returned
396
    @param filter_allocatable: whether to skip over unallocatable PVs
397

398
    @rtype: list
399
    @return: list of tuples (free_space, name) with free_space in mebibytes
400

401
    """
402
    sep = "|"
403
    command = ["pvs", "--noheadings", "--nosuffix", "--units=m",
404
               "-opv_name,vg_name,pv_free,pv_attr", "--unbuffered",
405
               "--separator=%s" % sep ]
406
    result = utils.RunCmd(command)
407
    if result.failed:
408
      logging.error("Can't get the PV information: %s - %s",
409
                    result.fail_reason, result.output)
410
      return None
411
    data = []
412
    for line in result.stdout.splitlines():
413
      fields = line.strip().split(sep)
414
      if len(fields) != 4:
415
        logging.error("Can't parse pvs output: line '%s'", line)
416
        return None
417
      # (possibly) skip over pvs which are not allocatable
418
      if filter_allocatable and fields[3][0] != 'a':
419
        continue
420
      # (possibly) skip over pvs which are not in the right volume group(s)
421
      if vg_names and fields[1] not in vg_names:
422
        continue
423
      data.append((float(fields[2]), fields[0], fields[1]))
424

    
425
    return data
426

    
427
  def Remove(self):
428
    """Remove this logical volume.
429

430
    """
431
    if not self.minor and not self.Attach():
432
      # the LV does not exist
433
      return
434
    result = utils.RunCmd(["lvremove", "-f", "%s/%s" %
435
                           (self._vg_name, self._lv_name)])
436
    if result.failed:
437
      _ThrowError("Can't lvremove: %s - %s", result.fail_reason, result.output)
438

    
439
  def Rename(self, new_id):
440
    """Rename this logical volume.
441

442
    """
443
    if not isinstance(new_id, (tuple, list)) or len(new_id) != 2:
444
      raise errors.ProgrammerError("Invalid new logical id '%s'" % new_id)
445
    new_vg, new_name = new_id
446
    if new_vg != self._vg_name:
447
      raise errors.ProgrammerError("Can't move a logical volume across"
448
                                   " volume groups (from %s to to %s)" %
449
                                   (self._vg_name, new_vg))
450
    result = utils.RunCmd(["lvrename", new_vg, self._lv_name, new_name])
451
    if result.failed:
452
      _ThrowError("Failed to rename the logical volume: %s", result.output)
453
    self._lv_name = new_name
454
    self.dev_path = "/dev/%s/%s" % (self._vg_name, self._lv_name)
455

    
456
  def Attach(self):
457
    """Attach to an existing LV.
458

459
    This method will try to see if an existing and active LV exists
460
    which matches our name. If so, its major/minor will be
461
    recorded.
462

463
    """
464
    self.attached = False
465
    result = utils.RunCmd(["lvs", "--noheadings", "--separator=,",
466
                           "--units=m", "--nosuffix",
467
                           "-olv_attr,lv_kernel_major,lv_kernel_minor,"
468
                           "vg_extent_size,stripes", self.dev_path])
469
    if result.failed:
470
      logging.error("Can't find LV %s: %s, %s",
471
                    self.dev_path, result.fail_reason, result.output)
472
      return False
473
    # the output can (and will) have multiple lines for multi-segment
474
    # LVs, as the 'stripes' parameter is a segment one, so we take
475
    # only the last entry, which is the one we're interested in; note
476
    # that with LVM2 anyway the 'stripes' value must be constant
477
    # across segments, so this is a no-op actually
478
    out = result.stdout.splitlines()
479
    if not out: # totally empty result? splitlines() returns at least
480
                # one line for any non-empty string
481
      logging.error("Can't parse LVS output, no lines? Got '%s'", str(out))
482
      return False
483
    out = out[-1].strip().rstrip(',')
484
    out = out.split(",")
485
    if len(out) != 5:
486
      logging.error("Can't parse LVS output, len(%s) != 5", str(out))
487
      return False
488

    
489
    status, major, minor, pe_size, stripes = out
490
    if len(status) != 6:
491
      logging.error("lvs lv_attr is not 6 characters (%s)", status)
492
      return False
493

    
494
    try:
495
      major = int(major)
496
      minor = int(minor)
497
    except ValueError, err:
498
      logging.error("lvs major/minor cannot be parsed: %s", str(err))
499

    
500
    try:
501
      pe_size = int(float(pe_size))
502
    except (TypeError, ValueError), err:
503
      logging.error("Can't parse vg extent size: %s", err)
504
      return False
505

    
506
    try:
507
      stripes = int(stripes)
508
    except (TypeError, ValueError), err:
509
      logging.error("Can't parse the number of stripes: %s", err)
510
      return False
511

    
512
    self.major = major
513
    self.minor = minor
514
    self.pe_size = pe_size
515
    self.stripe_count = stripes
516
    self._degraded = status[0] == 'v' # virtual volume, i.e. doesn't backing
517
                                      # storage
518
    self.attached = True
519
    return True
520

    
521
  def Assemble(self):
522
    """Assemble the device.
523

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

528
    """
529
    result = utils.RunCmd(["lvchange", "-ay", self.dev_path])
530
    if result.failed:
531
      _ThrowError("Can't activate lv %s: %s", self.dev_path, result.output)
532

    
533
  def Shutdown(self):
534
    """Shutdown the device.
535

536
    This is a no-op for the LV device type, as we don't deactivate the
537
    volumes on shutdown.
538

539
    """
540
    pass
541

    
542
  def GetSyncStatus(self):
543
    """Returns the sync status of the device.
544

545
    If this device is a mirroring device, this function returns the
546
    status of the mirror.
547

548
    For logical volumes, sync_percent and estimated_time are always
549
    None (no recovery in progress, as we don't handle the mirrored LV
550
    case). The is_degraded parameter is the inverse of the ldisk
551
    parameter.
552

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

559
    The status was already read in Attach, so we just return it.
560

561
    @rtype: objects.BlockDevStatus
562

563
    """
564
    if self._degraded:
565
      ldisk_status = constants.LDS_FAULTY
566
    else:
567
      ldisk_status = constants.LDS_OKAY
568

    
569
    return objects.BlockDevStatus(dev_path=self.dev_path,
570
                                  major=self.major,
571
                                  minor=self.minor,
572
                                  sync_percent=None,
573
                                  estimated_time=None,
574
                                  is_degraded=self._degraded,
575
                                  ldisk_status=ldisk_status)
576

    
577
  def Open(self, force=False):
578
    """Make the device ready for I/O.
579

580
    This is a no-op for the LV device type.
581

582
    """
583
    pass
584

    
585
  def Close(self):
586
    """Notifies that the device will no longer be used for I/O.
587

588
    This is a no-op for the LV device type.
589

590
    """
591
    pass
592

    
593
  def Snapshot(self, size):
594
    """Create a snapshot copy of an lvm block device.
595

596
    """
597
    snap_name = self._lv_name + ".snap"
598

    
599
    # remove existing snapshot if found
600
    snap = LogicalVolume((self._vg_name, snap_name), None, size)
601
    _IgnoreError(snap.Remove)
602

    
603
    pvs_info = self.GetPVInfo([self._vg_name])
604
    if not pvs_info:
605
      _ThrowError("Can't compute PV info for vg %s", self._vg_name)
606
    pvs_info.sort()
607
    pvs_info.reverse()
608
    free_size, pv_name, _ = pvs_info[0]
609
    if free_size < size:
610
      _ThrowError("Not enough free space: required %s,"
611
                  " available %s", size, free_size)
612

    
613
    result = utils.RunCmd(["lvcreate", "-L%dm" % size, "-s",
614
                           "-n%s" % snap_name, self.dev_path])
615
    if result.failed:
616
      _ThrowError("command: %s error: %s - %s",
617
                  result.cmd, result.fail_reason, result.output)
618

    
619
    return snap_name
620

    
621
  def SetInfo(self, text):
622
    """Update metadata with info text.
623

624
    """
625
    BlockDev.SetInfo(self, text)
626

    
627
    # Replace invalid characters
628
    text = re.sub('^[^A-Za-z0-9_+.]', '_', text)
629
    text = re.sub('[^-A-Za-z0-9_+.]', '_', text)
630

    
631
    # Only up to 128 characters are allowed
632
    text = text[:128]
633

    
634
    result = utils.RunCmd(["lvchange", "--addtag", text,
635
                           self.dev_path])
636
    if result.failed:
637
      _ThrowError("Command: %s error: %s - %s", result.cmd, result.fail_reason,
638
                  result.output)
639

    
640
  def Grow(self, amount):
641
    """Grow the logical volume.
642

643
    """
644
    if self.pe_size is None or self.stripe_count is None:
645
      if not self.Attach():
646
        _ThrowError("Can't attach to LV during Grow()")
647
    full_stripe_size = self.pe_size * self.stripe_count
648
    rest = amount % full_stripe_size
649
    if rest != 0:
650
      amount += full_stripe_size - rest
651
    # we try multiple algorithms since the 'best' ones might not have
652
    # space available in the right place, but later ones might (since
653
    # they have less constraints); also note that only recent LVM
654
    # supports 'cling'
655
    for alloc_policy in "contiguous", "cling", "normal":
656
      result = utils.RunCmd(["lvextend", "--alloc", alloc_policy,
657
                             "-L", "+%dm" % amount, self.dev_path])
658
      if not result.failed:
659
        return
660
    _ThrowError("Can't grow LV %s: %s", self.dev_path, result.output)
661

    
662

    
663
class DRBD8Status(object):
664
  """A DRBD status representation class.
665

666
  Note that this doesn't support unconfigured devices (cs:Unconfigured).
667

668
  """
669
  UNCONF_RE = re.compile(r"\s*[0-9]+:\s*cs:Unconfigured$")
670
  LINE_RE = re.compile(r"\s*[0-9]+:\s*cs:(\S+)\s+(?:st|ro):([^/]+)/(\S+)"
671
                       "\s+ds:([^/]+)/(\S+)\s+.*$")
672
  SYNC_RE = re.compile(r"^.*\ssync'ed:\s*([0-9.]+)%.*"
673
                       "\sfinish: ([0-9]+):([0-9]+):([0-9]+)\s.*$")
674

    
675
  CS_UNCONFIGURED = "Unconfigured"
676
  CS_STANDALONE = "StandAlone"
677
  CS_WFCONNECTION = "WFConnection"
678
  CS_WFREPORTPARAMS = "WFReportParams"
679
  CS_CONNECTED = "Connected"
680
  CS_STARTINGSYNCS = "StartingSyncS"
681
  CS_STARTINGSYNCT = "StartingSyncT"
682
  CS_WFBITMAPS = "WFBitMapS"
683
  CS_WFBITMAPT = "WFBitMapT"
684
  CS_WFSYNCUUID = "WFSyncUUID"
685
  CS_SYNCSOURCE = "SyncSource"
686
  CS_SYNCTARGET = "SyncTarget"
687
  CS_PAUSEDSYNCS = "PausedSyncS"
688
  CS_PAUSEDSYNCT = "PausedSyncT"
689
  CSET_SYNC = frozenset([
690
    CS_WFREPORTPARAMS,
691
    CS_STARTINGSYNCS,
692
    CS_STARTINGSYNCT,
693
    CS_WFBITMAPS,
694
    CS_WFBITMAPT,
695
    CS_WFSYNCUUID,
696
    CS_SYNCSOURCE,
697
    CS_SYNCTARGET,
698
    CS_PAUSEDSYNCS,
699
    CS_PAUSEDSYNCT,
700
    ])
701

    
702
  DS_DISKLESS = "Diskless"
703
  DS_ATTACHING = "Attaching" # transient state
704
  DS_FAILED = "Failed" # transient state, next: diskless
705
  DS_NEGOTIATING = "Negotiating" # transient state
706
  DS_INCONSISTENT = "Inconsistent" # while syncing or after creation
707
  DS_OUTDATED = "Outdated"
708
  DS_DUNKNOWN = "DUnknown" # shown for peer disk when not connected
709
  DS_CONSISTENT = "Consistent"
710
  DS_UPTODATE = "UpToDate" # normal state
711

    
712
  RO_PRIMARY = "Primary"
713
  RO_SECONDARY = "Secondary"
714
  RO_UNKNOWN = "Unknown"
715

    
716
  def __init__(self, procline):
717
    u = self.UNCONF_RE.match(procline)
718
    if u:
719
      self.cstatus = self.CS_UNCONFIGURED
720
      self.lrole = self.rrole = self.ldisk = self.rdisk = None
721
    else:
722
      m = self.LINE_RE.match(procline)
723
      if not m:
724
        raise errors.BlockDeviceError("Can't parse input data '%s'" % procline)
725
      self.cstatus = m.group(1)
726
      self.lrole = m.group(2)
727
      self.rrole = m.group(3)
728
      self.ldisk = m.group(4)
729
      self.rdisk = m.group(5)
730

    
731
    # end reading of data from the LINE_RE or UNCONF_RE
732

    
733
    self.is_standalone = self.cstatus == self.CS_STANDALONE
734
    self.is_wfconn = self.cstatus == self.CS_WFCONNECTION
735
    self.is_connected = self.cstatus == self.CS_CONNECTED
736
    self.is_primary = self.lrole == self.RO_PRIMARY
737
    self.is_secondary = self.lrole == self.RO_SECONDARY
738
    self.peer_primary = self.rrole == self.RO_PRIMARY
739
    self.peer_secondary = self.rrole == self.RO_SECONDARY
740
    self.both_primary = self.is_primary and self.peer_primary
741
    self.both_secondary = self.is_secondary and self.peer_secondary
742

    
743
    self.is_diskless = self.ldisk == self.DS_DISKLESS
744
    self.is_disk_uptodate = self.ldisk == self.DS_UPTODATE
745

    
746
    self.is_in_resync = self.cstatus in self.CSET_SYNC
747
    self.is_in_use = self.cstatus != self.CS_UNCONFIGURED
748

    
749
    m = self.SYNC_RE.match(procline)
750
    if m:
751
      self.sync_percent = float(m.group(1))
752
      hours = int(m.group(2))
753
      minutes = int(m.group(3))
754
      seconds = int(m.group(4))
755
      self.est_time = hours * 3600 + minutes * 60 + seconds
756
    else:
757
      # we have (in this if branch) no percent information, but if
758
      # we're resyncing we need to 'fake' a sync percent information,
759
      # as this is how cmdlib determines if it makes sense to wait for
760
      # resyncing or not
761
      if self.is_in_resync:
762
        self.sync_percent = 0
763
      else:
764
        self.sync_percent = None
765
      self.est_time = None
766

    
767

    
768
class BaseDRBD(BlockDev):
769
  """Base DRBD class.
770

771
  This class contains a few bits of common functionality between the
772
  0.7 and 8.x versions of DRBD.
773

774
  """
775
  _VERSION_RE = re.compile(r"^version: (\d+)\.(\d+)\.(\d+)"
776
                           r" \(api:(\d+)/proto:(\d+)(?:-(\d+))?\)")
777

    
778
  _DRBD_MAJOR = 147
779
  _ST_UNCONFIGURED = "Unconfigured"
780
  _ST_WFCONNECTION = "WFConnection"
781
  _ST_CONNECTED = "Connected"
782

    
783
  _STATUS_FILE = "/proc/drbd"
784

    
785
  @staticmethod
786
  def _GetProcData(filename=_STATUS_FILE):
787
    """Return data from /proc/drbd.
788

789
    """
790
    try:
791
      data = utils.ReadFile(filename).splitlines()
792
    except EnvironmentError, err:
793
      if err.errno == errno.ENOENT:
794
        _ThrowError("The file %s cannot be opened, check if the module"
795
                    " is loaded (%s)", filename, str(err))
796
      else:
797
        _ThrowError("Can't read the DRBD proc file %s: %s", filename, str(err))
798
    if not data:
799
      _ThrowError("Can't read any data from %s", filename)
800
    return data
801

    
802
  @staticmethod
803
  def _MassageProcData(data):
804
    """Transform the output of _GetProdData into a nicer form.
805

806
    @return: a dictionary of minor: joined lines from /proc/drbd
807
        for that minor
808

809
    """
810
    lmatch = re.compile("^ *([0-9]+):.*$")
811
    results = {}
812
    old_minor = old_line = None
813
    for line in data:
814
      if not line: # completely empty lines, as can be returned by drbd8.0+
815
        continue
816
      lresult = lmatch.match(line)
817
      if lresult is not None:
818
        if old_minor is not None:
819
          results[old_minor] = old_line
820
        old_minor = int(lresult.group(1))
821
        old_line = line
822
      else:
823
        if old_minor is not None:
824
          old_line += " " + line.strip()
825
    # add last line
826
    if old_minor is not None:
827
      results[old_minor] = old_line
828
    return results
829

    
830
  @classmethod
831
  def _GetVersion(cls):
832
    """Return the DRBD version.
833

834
    This will return a dict with keys:
835
      - k_major
836
      - k_minor
837
      - k_point
838
      - api
839
      - proto
840
      - proto2 (only on drbd > 8.2.X)
841

842
    """
843
    proc_data = cls._GetProcData()
844
    first_line = proc_data[0].strip()
845
    version = cls._VERSION_RE.match(first_line)
846
    if not version:
847
      raise errors.BlockDeviceError("Can't parse DRBD version from '%s'" %
848
                                    first_line)
849

    
850
    values = version.groups()
851
    retval = {'k_major': int(values[0]),
852
              'k_minor': int(values[1]),
853
              'k_point': int(values[2]),
854
              'api': int(values[3]),
855
              'proto': int(values[4]),
856
             }
857
    if values[5] is not None:
858
      retval['proto2'] = values[5]
859

    
860
    return retval
861

    
862
  @staticmethod
863
  def _DevPath(minor):
864
    """Return the path to a drbd device for a given minor.
865

866
    """
867
    return "/dev/drbd%d" % minor
868

    
869
  @classmethod
870
  def GetUsedDevs(cls):
871
    """Compute the list of used DRBD devices.
872

873
    """
874
    data = cls._GetProcData()
875

    
876
    used_devs = {}
877
    valid_line = re.compile("^ *([0-9]+): cs:([^ ]+).*$")
878
    for line in data:
879
      match = valid_line.match(line)
880
      if not match:
881
        continue
882
      minor = int(match.group(1))
883
      state = match.group(2)
884
      if state == cls._ST_UNCONFIGURED:
885
        continue
886
      used_devs[minor] = state, line
887

    
888
    return used_devs
889

    
890
  def _SetFromMinor(self, minor):
891
    """Set our parameters based on the given minor.
892

893
    This sets our minor variable and our dev_path.
894

895
    """
896
    if minor is None:
897
      self.minor = self.dev_path = None
898
      self.attached = False
899
    else:
900
      self.minor = minor
901
      self.dev_path = self._DevPath(minor)
902
      self.attached = True
903

    
904
  @staticmethod
905
  def _CheckMetaSize(meta_device):
906
    """Check if the given meta device looks like a valid one.
907

908
    This currently only check the size, which must be around
909
    128MiB.
910

911
    """
912
    result = utils.RunCmd(["blockdev", "--getsize", meta_device])
913
    if result.failed:
914
      _ThrowError("Failed to get device size: %s - %s",
915
                  result.fail_reason, result.output)
916
    try:
917
      sectors = int(result.stdout)
918
    except ValueError:
919
      _ThrowError("Invalid output from blockdev: '%s'", result.stdout)
920
    bytes = sectors * 512
921
    if bytes < 128 * 1024 * 1024: # less than 128MiB
922
      _ThrowError("Meta device too small (%.2fMib)", (bytes / 1024 / 1024))
923
    # the maximum *valid* size of the meta device when living on top
924
    # of LVM is hard to compute: it depends on the number of stripes
925
    # and the PE size; e.g. a 2-stripe, 64MB PE will result in a 128MB
926
    # (normal size), but an eight-stripe 128MB PE will result in a 1GB
927
    # size meta device; as such, we restrict it to 1GB (a little bit
928
    # too generous, but making assumptions about PE size is hard)
929
    if bytes > 1024 * 1024 * 1024:
930
      _ThrowError("Meta device too big (%.2fMiB)", (bytes / 1024 / 1024))
931

    
932
  def Rename(self, new_id):
933
    """Rename a device.
934

935
    This is not supported for drbd devices.
936

937
    """
938
    raise errors.ProgrammerError("Can't rename a drbd device")
939

    
940

    
941
class DRBD8(BaseDRBD):
942
  """DRBD v8.x block device.
943

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

948
  The unique_id for the drbd device is the (local_ip, local_port,
949
  remote_ip, remote_port) tuple, and it must have two children: the
950
  data device and the meta_device. The meta device is checked for
951
  valid size and is zeroed on create.
952

953
  """
954
  _MAX_MINORS = 255
955
  _PARSE_SHOW = None
956

    
957
  # timeout constants
958
  _NET_RECONFIG_TIMEOUT = 60
959

    
960
  def __init__(self, unique_id, children, size):
961
    if children and children.count(None) > 0:
962
      children = []
963
    super(DRBD8, self).__init__(unique_id, children, size)
964
    self.major = self._DRBD_MAJOR
965
    version = self._GetVersion()
966
    if version['k_major'] != 8 :
967
      _ThrowError("Mismatch in DRBD kernel version and requested ganeti"
968
                  " usage: kernel is %s.%s, ganeti wants 8.x",
969
                  version['k_major'], version['k_minor'])
970

    
971
    if len(children) not in (0, 2):
972
      raise ValueError("Invalid configuration data %s" % str(children))
973
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 6:
974
      raise ValueError("Invalid configuration data %s" % str(unique_id))
975
    (self._lhost, self._lport,
976
     self._rhost, self._rport,
977
     self._aminor, self._secret) = unique_id
978
    if (self._lhost is not None and self._lhost == self._rhost and
979
        self._lport == self._rport):
980
      raise ValueError("Invalid configuration data, same local/remote %s" %
981
                       (unique_id,))
982
    self.Attach()
983

    
984
  @classmethod
985
  def _InitMeta(cls, minor, dev_path):
986
    """Initialize a meta device.
987

988
    This will not work if the given minor is in use.
989

990
    """
991
    result = utils.RunCmd(["drbdmeta", "--force", cls._DevPath(minor),
992
                           "v08", dev_path, "0", "create-md"])
993
    if result.failed:
994
      _ThrowError("Can't initialize meta device: %s", result.output)
995

    
996
  @classmethod
997
  def _FindUnusedMinor(cls):
998
    """Find an unused DRBD device.
999

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

1003
    """
1004
    data = cls._GetProcData()
1005

    
1006
    unused_line = re.compile("^ *([0-9]+): cs:Unconfigured$")
1007
    used_line = re.compile("^ *([0-9]+): cs:")
1008
    highest = None
1009
    for line in data:
1010
      match = unused_line.match(line)
1011
      if match:
1012
        return int(match.group(1))
1013
      match = used_line.match(line)
1014
      if match:
1015
        minor = int(match.group(1))
1016
        highest = max(highest, minor)
1017
    if highest is None: # there are no minors in use at all
1018
      return 0
1019
    if highest >= cls._MAX_MINORS:
1020
      logging.error("Error: no free drbd minors!")
1021
      raise errors.BlockDeviceError("Can't find a free DRBD minor")
1022
    return highest + 1
1023

    
1024
  @classmethod
1025
  def _GetShowParser(cls):
1026
    """Return a parser for `drbd show` output.
1027

1028
    This will either create or return an already-create parser for the
1029
    output of the command `drbd show`.
1030

1031
    """
1032
    if cls._PARSE_SHOW is not None:
1033
      return cls._PARSE_SHOW
1034

    
1035
    # pyparsing setup
1036
    lbrace = pyp.Literal("{").suppress()
1037
    rbrace = pyp.Literal("}").suppress()
1038
    semi = pyp.Literal(";").suppress()
1039
    # this also converts the value to an int
1040
    number = pyp.Word(pyp.nums).setParseAction(lambda s, l, t: int(t[0]))
1041

    
1042
    comment = pyp.Literal ("#") + pyp.Optional(pyp.restOfLine)
1043
    defa = pyp.Literal("_is_default").suppress()
1044
    dbl_quote = pyp.Literal('"').suppress()
1045

    
1046
    keyword = pyp.Word(pyp.alphanums + '-')
1047

    
1048
    # value types
1049
    value = pyp.Word(pyp.alphanums + '_-/.:')
1050
    quoted = dbl_quote + pyp.CharsNotIn('"') + dbl_quote
1051
    addr_type = (pyp.Optional(pyp.Literal("ipv4")).suppress() +
1052
                 pyp.Optional(pyp.Literal("ipv6")).suppress())
1053
    addr_port = (addr_type + pyp.Word(pyp.nums + '.') +
1054
                 pyp.Literal(':').suppress() + number)
1055
    # meta device, extended syntax
1056
    meta_value = ((value ^ quoted) + pyp.Literal('[').suppress() +
1057
                  number + pyp.Word(']').suppress())
1058
    # device name, extended syntax
1059
    device_value = pyp.Literal("minor").suppress() + number
1060

    
1061
    # a statement
1062
    stmt = (~rbrace + keyword + ~lbrace +
1063
            pyp.Optional(addr_port ^ value ^ quoted ^ meta_value ^
1064
                         device_value) +
1065
            pyp.Optional(defa) + semi +
1066
            pyp.Optional(pyp.restOfLine).suppress())
1067

    
1068
    # an entire section
1069
    section_name = pyp.Word(pyp.alphas + '_')
1070
    section = section_name + lbrace + pyp.ZeroOrMore(pyp.Group(stmt)) + rbrace
1071

    
1072
    bnf = pyp.ZeroOrMore(pyp.Group(section ^ stmt))
1073
    bnf.ignore(comment)
1074

    
1075
    cls._PARSE_SHOW = bnf
1076

    
1077
    return bnf
1078

    
1079
  @classmethod
1080
  def _GetShowData(cls, minor):
1081
    """Return the `drbdsetup show` data for a minor.
1082

1083
    """
1084
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "show"])
1085
    if result.failed:
1086
      logging.error("Can't display the drbd config: %s - %s",
1087
                    result.fail_reason, result.output)
1088
      return None
1089
    return result.stdout
1090

    
1091
  @classmethod
1092
  def _GetDevInfo(cls, out):
1093
    """Parse details about a given DRBD minor.
1094

1095
    This return, if available, the local backing device (as a path)
1096
    and the local and remote (ip, port) information from a string
1097
    containing the output of the `drbdsetup show` command as returned
1098
    by _GetShowData.
1099

1100
    """
1101
    data = {}
1102
    if not out:
1103
      return data
1104

    
1105
    bnf = cls._GetShowParser()
1106
    # run pyparse
1107

    
1108
    try:
1109
      results = bnf.parseString(out)
1110
    except pyp.ParseException, err:
1111
      _ThrowError("Can't parse drbdsetup show output: %s", str(err))
1112

    
1113
    # and massage the results into our desired format
1114
    for section in results:
1115
      sname = section[0]
1116
      if sname == "_this_host":
1117
        for lst in section[1:]:
1118
          if lst[0] == "disk":
1119
            data["local_dev"] = lst[1]
1120
          elif lst[0] == "meta-disk":
1121
            data["meta_dev"] = lst[1]
1122
            data["meta_index"] = lst[2]
1123
          elif lst[0] == "address":
1124
            data["local_addr"] = tuple(lst[1:])
1125
      elif sname == "_remote_host":
1126
        for lst in section[1:]:
1127
          if lst[0] == "address":
1128
            data["remote_addr"] = tuple(lst[1:])
1129
    return data
1130

    
1131
  def _MatchesLocal(self, info):
1132
    """Test if our local config matches with an existing device.
1133

1134
    The parameter should be as returned from `_GetDevInfo()`. This
1135
    method tests if our local backing device is the same as the one in
1136
    the info parameter, in effect testing if we look like the given
1137
    device.
1138

1139
    """
1140
    if self._children:
1141
      backend, meta = self._children
1142
    else:
1143
      backend = meta = None
1144

    
1145
    if backend is not None:
1146
      retval = ("local_dev" in info and info["local_dev"] == backend.dev_path)
1147
    else:
1148
      retval = ("local_dev" not in info)
1149

    
1150
    if meta is not None:
1151
      retval = retval and ("meta_dev" in info and
1152
                           info["meta_dev"] == meta.dev_path)
1153
      retval = retval and ("meta_index" in info and
1154
                           info["meta_index"] == 0)
1155
    else:
1156
      retval = retval and ("meta_dev" not in info and
1157
                           "meta_index" not in info)
1158
    return retval
1159

    
1160
  def _MatchesNet(self, info):
1161
    """Test if our network config matches with an existing device.
1162

1163
    The parameter should be as returned from `_GetDevInfo()`. This
1164
    method tests if our network configuration is the same as the one
1165
    in the info parameter, in effect testing if we look like the given
1166
    device.
1167

1168
    """
1169
    if (((self._lhost is None and not ("local_addr" in info)) and
1170
         (self._rhost is None and not ("remote_addr" in info)))):
1171
      return True
1172

    
1173
    if self._lhost is None:
1174
      return False
1175

    
1176
    if not ("local_addr" in info and
1177
            "remote_addr" in info):
1178
      return False
1179

    
1180
    retval = (info["local_addr"] == (self._lhost, self._lport))
1181
    retval = (retval and
1182
              info["remote_addr"] == (self._rhost, self._rport))
1183
    return retval
1184

    
1185
  @classmethod
1186
  def _AssembleLocal(cls, minor, backend, meta, size):
1187
    """Configure the local part of a DRBD device.
1188

1189
    """
1190
    args = ["drbdsetup", cls._DevPath(minor), "disk",
1191
            backend, meta, "0",
1192
            "-e", "detach",
1193
            "--create-device"]
1194
    if size:
1195
      args.extend(["-d", "%sm" % size])
1196
    result = utils.RunCmd(args)
1197
    if result.failed:
1198
      _ThrowError("drbd%d: can't attach local disk: %s", minor, result.output)
1199

    
1200
  @classmethod
1201
  def _AssembleNet(cls, minor, net_info, protocol,
1202
                   dual_pri=False, hmac=None, secret=None):
1203
    """Configure the network part of the device.
1204

1205
    """
1206
    lhost, lport, rhost, rport = net_info
1207
    if None in net_info:
1208
      # we don't want network connection and actually want to make
1209
      # sure its shutdown
1210
      cls._ShutdownNet(minor)
1211
      return
1212

    
1213
    # Workaround for a race condition. When DRBD is doing its dance to
1214
    # establish a connection with its peer, it also sends the
1215
    # synchronization speed over the wire. In some cases setting the
1216
    # sync speed only after setting up both sides can race with DRBD
1217
    # connecting, hence we set it here before telling DRBD anything
1218
    # about its peer.
1219
    cls._SetMinorSyncSpeed(minor, constants.SYNC_SPEED)
1220

    
1221
    args = ["drbdsetup", cls._DevPath(minor), "net",
1222
            "%s:%s" % (lhost, lport), "%s:%s" % (rhost, rport), protocol,
1223
            "-A", "discard-zero-changes",
1224
            "-B", "consensus",
1225
            "--create-device",
1226
            ]
1227
    if dual_pri:
1228
      args.append("-m")
1229
    if hmac and secret:
1230
      args.extend(["-a", hmac, "-x", secret])
1231
    result = utils.RunCmd(args)
1232
    if result.failed:
1233
      _ThrowError("drbd%d: can't setup network: %s - %s",
1234
                  minor, result.fail_reason, result.output)
1235

    
1236
    def _CheckNetworkConfig():
1237
      info = cls._GetDevInfo(cls._GetShowData(minor))
1238
      if not "local_addr" in info or not "remote_addr" in info:
1239
        raise utils.RetryAgain()
1240

    
1241
      if (info["local_addr"] != (lhost, lport) or
1242
          info["remote_addr"] != (rhost, rport)):
1243
        raise utils.RetryAgain()
1244

    
1245
    try:
1246
      utils.Retry(_CheckNetworkConfig, 1.0, 10.0)
1247
    except utils.RetryTimeout:
1248
      _ThrowError("drbd%d: timeout while configuring network", minor)
1249

    
1250
  def AddChildren(self, devices):
1251
    """Add a disk to the DRBD device.
1252

1253
    """
1254
    if self.minor is None:
1255
      _ThrowError("drbd%d: can't attach to dbrd8 during AddChildren",
1256
                  self._aminor)
1257
    if len(devices) != 2:
1258
      _ThrowError("drbd%d: need two devices for AddChildren", self.minor)
1259
    info = self._GetDevInfo(self._GetShowData(self.minor))
1260
    if "local_dev" in info:
1261
      _ThrowError("drbd%d: already attached to a local disk", self.minor)
1262
    backend, meta = devices
1263
    if backend.dev_path is None or meta.dev_path is None:
1264
      _ThrowError("drbd%d: children not ready during AddChildren", self.minor)
1265
    backend.Open()
1266
    meta.Open()
1267
    self._CheckMetaSize(meta.dev_path)
1268
    self._InitMeta(self._FindUnusedMinor(), meta.dev_path)
1269

    
1270
    self._AssembleLocal(self.minor, backend.dev_path, meta.dev_path, self.size)
1271
    self._children = devices
1272

    
1273
  def RemoveChildren(self, devices):
1274
    """Detach the drbd device from local storage.
1275

1276
    """
1277
    if self.minor is None:
1278
      _ThrowError("drbd%d: can't attach to drbd8 during RemoveChildren",
1279
                  self._aminor)
1280
    # early return if we don't actually have backing storage
1281
    info = self._GetDevInfo(self._GetShowData(self.minor))
1282
    if "local_dev" not in info:
1283
      return
1284
    if len(self._children) != 2:
1285
      _ThrowError("drbd%d: we don't have two children: %s", self.minor,
1286
                  self._children)
1287
    if self._children.count(None) == 2: # we don't actually have children :)
1288
      logging.warning("drbd%d: requested detach while detached", self.minor)
1289
      return
1290
    if len(devices) != 2:
1291
      _ThrowError("drbd%d: we need two children in RemoveChildren", self.minor)
1292
    for child, dev in zip(self._children, devices):
1293
      if dev != child.dev_path:
1294
        _ThrowError("drbd%d: mismatch in local storage (%s != %s) in"
1295
                    " RemoveChildren", self.minor, dev, child.dev_path)
1296

    
1297
    self._ShutdownLocal(self.minor)
1298
    self._children = []
1299

    
1300
  @classmethod
1301
  def _SetMinorSyncSpeed(cls, minor, kbytes):
1302
    """Set the speed of the DRBD syncer.
1303

1304
    This is the low-level implementation.
1305

1306
    @type minor: int
1307
    @param minor: the drbd minor whose settings we change
1308
    @type kbytes: int
1309
    @param kbytes: the speed in kbytes/second
1310
    @rtype: boolean
1311
    @return: the success of the operation
1312

1313
    """
1314
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "syncer",
1315
                           "-r", "%d" % kbytes, "--create-device"])
1316
    if result.failed:
1317
      logging.error("Can't change syncer rate: %s - %s",
1318
                    result.fail_reason, result.output)
1319
    return not result.failed
1320

    
1321
  def SetSyncSpeed(self, kbytes):
1322
    """Set the speed of the DRBD syncer.
1323

1324
    @type kbytes: int
1325
    @param kbytes: the speed in kbytes/second
1326
    @rtype: boolean
1327
    @return: the success of the operation
1328

1329
    """
1330
    if self.minor is None:
1331
      logging.info("Not attached during SetSyncSpeed")
1332
      return False
1333
    children_result = super(DRBD8, self).SetSyncSpeed(kbytes)
1334
    return self._SetMinorSyncSpeed(self.minor, kbytes) and children_result
1335

    
1336
  def GetProcStatus(self):
1337
    """Return device data from /proc.
1338

1339
    """
1340
    if self.minor is None:
1341
      _ThrowError("drbd%d: GetStats() called while not attached", self._aminor)
1342
    proc_info = self._MassageProcData(self._GetProcData())
1343
    if self.minor not in proc_info:
1344
      _ThrowError("drbd%d: can't find myself in /proc", self.minor)
1345
    return DRBD8Status(proc_info[self.minor])
1346

    
1347
  def GetSyncStatus(self):
1348
    """Returns the sync status of the device.
1349

1350

1351
    If sync_percent is None, it means all is ok
1352
    If estimated_time is None, it means we can't estimate
1353
    the time needed, otherwise it's the time left in seconds.
1354

1355

1356
    We set the is_degraded parameter to True on two conditions:
1357
    network not connected or local disk missing.
1358

1359
    We compute the ldisk parameter based on whether we have a local
1360
    disk or not.
1361

1362
    @rtype: objects.BlockDevStatus
1363

1364
    """
1365
    if self.minor is None and not self.Attach():
1366
      _ThrowError("drbd%d: can't Attach() in GetSyncStatus", self._aminor)
1367

    
1368
    stats = self.GetProcStatus()
1369
    is_degraded = not stats.is_connected or not stats.is_disk_uptodate
1370

    
1371
    if stats.is_disk_uptodate:
1372
      ldisk_status = constants.LDS_OKAY
1373
    elif stats.is_diskless:
1374
      ldisk_status = constants.LDS_FAULTY
1375
    else:
1376
      ldisk_status = constants.LDS_UNKNOWN
1377

    
1378
    return objects.BlockDevStatus(dev_path=self.dev_path,
1379
                                  major=self.major,
1380
                                  minor=self.minor,
1381
                                  sync_percent=stats.sync_percent,
1382
                                  estimated_time=stats.est_time,
1383
                                  is_degraded=is_degraded,
1384
                                  ldisk_status=ldisk_status)
1385

    
1386
  def Open(self, force=False):
1387
    """Make the local state primary.
1388

1389
    If the 'force' parameter is given, the '-o' option is passed to
1390
    drbdsetup. Since this is a potentially dangerous operation, the
1391
    force flag should be only given after creation, when it actually
1392
    is mandatory.
1393

1394
    """
1395
    if self.minor is None and not self.Attach():
1396
      logging.error("DRBD cannot attach to a device during open")
1397
      return False
1398
    cmd = ["drbdsetup", self.dev_path, "primary"]
1399
    if force:
1400
      cmd.append("-o")
1401
    result = utils.RunCmd(cmd)
1402
    if result.failed:
1403
      _ThrowError("drbd%d: can't make drbd device primary: %s", self.minor,
1404
                  result.output)
1405

    
1406
  def Close(self):
1407
    """Make the local state secondary.
1408

1409
    This will, of course, fail if the device is in use.
1410

1411
    """
1412
    if self.minor is None and not self.Attach():
1413
      _ThrowError("drbd%d: can't Attach() in Close()", self._aminor)
1414
    result = utils.RunCmd(["drbdsetup", self.dev_path, "secondary"])
1415
    if result.failed:
1416
      _ThrowError("drbd%d: can't switch drbd device to secondary: %s",
1417
                  self.minor, result.output)
1418

    
1419
  def DisconnectNet(self):
1420
    """Removes network configuration.
1421

1422
    This method shutdowns the network side of the device.
1423

1424
    The method will wait up to a hardcoded timeout for the device to
1425
    go into standalone after the 'disconnect' command before
1426
    re-configuring it, as sometimes it takes a while for the
1427
    disconnect to actually propagate and thus we might issue a 'net'
1428
    command while the device is still connected. If the device will
1429
    still be attached to the network and we time out, we raise an
1430
    exception.
1431

1432
    """
1433
    if self.minor is None:
1434
      _ThrowError("drbd%d: disk not attached in re-attach net", self._aminor)
1435

    
1436
    if None in (self._lhost, self._lport, self._rhost, self._rport):
1437
      _ThrowError("drbd%d: DRBD disk missing network info in"
1438
                  " DisconnectNet()", self.minor)
1439

    
1440
    class _DisconnectStatus:
1441
      def __init__(self, ever_disconnected):
1442
        self.ever_disconnected = ever_disconnected
1443

    
1444
    dstatus = _DisconnectStatus(_IgnoreError(self._ShutdownNet, self.minor))
1445

    
1446
    def _WaitForDisconnect():
1447
      if self.GetProcStatus().is_standalone:
1448
        return
1449

    
1450
      # retry the disconnect, it seems possible that due to a well-time
1451
      # disconnect on the peer, my disconnect command might be ignored and
1452
      # forgotten
1453
      dstatus.ever_disconnected = \
1454
        _IgnoreError(self._ShutdownNet, self.minor) or dstatus.ever_disconnected
1455

    
1456
      raise utils.RetryAgain()
1457

    
1458
    # Keep start time
1459
    start_time = time.time()
1460

    
1461
    try:
1462
      # Start delay at 100 milliseconds and grow up to 2 seconds
1463
      utils.Retry(_WaitForDisconnect, (0.1, 1.5, 2.0),
1464
                  self._NET_RECONFIG_TIMEOUT)
1465
    except utils.RetryTimeout:
1466
      if dstatus.ever_disconnected:
1467
        msg = ("drbd%d: device did not react to the"
1468
               " 'disconnect' command in a timely manner")
1469
      else:
1470
        msg = "drbd%d: can't shutdown network, even after multiple retries"
1471

    
1472
      _ThrowError(msg, self.minor)
1473

    
1474
    reconfig_time = time.time() - start_time
1475
    if reconfig_time > (self._NET_RECONFIG_TIMEOUT * 0.25):
1476
      logging.info("drbd%d: DisconnectNet: detach took %.3f seconds",
1477
                   self.minor, reconfig_time)
1478

    
1479
  def AttachNet(self, multimaster):
1480
    """Reconnects the network.
1481

1482
    This method connects the network side of the device with a
1483
    specified multi-master flag. The device needs to be 'Standalone'
1484
    but have valid network configuration data.
1485

1486
    Args:
1487
      - multimaster: init the network in dual-primary mode
1488

1489
    """
1490
    if self.minor is None:
1491
      _ThrowError("drbd%d: device not attached in AttachNet", self._aminor)
1492

    
1493
    if None in (self._lhost, self._lport, self._rhost, self._rport):
1494
      _ThrowError("drbd%d: missing network info in AttachNet()", self.minor)
1495

    
1496
    status = self.GetProcStatus()
1497

    
1498
    if not status.is_standalone:
1499
      _ThrowError("drbd%d: device is not standalone in AttachNet", self.minor)
1500

    
1501
    self._AssembleNet(self.minor,
1502
                      (self._lhost, self._lport, self._rhost, self._rport),
1503
                      constants.DRBD_NET_PROTOCOL, dual_pri=multimaster,
1504
                      hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1505

    
1506
  def Attach(self):
1507
    """Check if our minor is configured.
1508

1509
    This doesn't do any device configurations - it only checks if the
1510
    minor is in a state different from Unconfigured.
1511

1512
    Note that this function will not change the state of the system in
1513
    any way (except in case of side-effects caused by reading from
1514
    /proc).
1515

1516
    """
1517
    used_devs = self.GetUsedDevs()
1518
    if self._aminor in used_devs:
1519
      minor = self._aminor
1520
    else:
1521
      minor = None
1522

    
1523
    self._SetFromMinor(minor)
1524
    return minor is not None
1525

    
1526
  def Assemble(self):
1527
    """Assemble the drbd.
1528

1529
    Method:
1530
      - if we have a configured device, we try to ensure that it matches
1531
        our config
1532
      - if not, we create it from zero
1533

1534
    """
1535
    super(DRBD8, self).Assemble()
1536

    
1537
    self.Attach()
1538
    if self.minor is None:
1539
      # local device completely unconfigured
1540
      self._FastAssemble()
1541
    else:
1542
      # we have to recheck the local and network status and try to fix
1543
      # the device
1544
      self._SlowAssemble()
1545

    
1546
  def _SlowAssemble(self):
1547
    """Assembles the DRBD device from a (partially) configured device.
1548

1549
    In case of partially attached (local device matches but no network
1550
    setup), we perform the network attach. If successful, we re-test
1551
    the attach if can return success.
1552

1553
    """
1554
    net_data = (self._lhost, self._lport, self._rhost, self._rport)
1555
    for minor in (self._aminor,):
1556
      info = self._GetDevInfo(self._GetShowData(minor))
1557
      match_l = self._MatchesLocal(info)
1558
      match_r = self._MatchesNet(info)
1559

    
1560
      if match_l and match_r:
1561
        # everything matches
1562
        break
1563

    
1564
      if match_l and not match_r and "local_addr" not in info:
1565
        # disk matches, but not attached to network, attach and recheck
1566
        self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
1567
                          hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1568
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1569
          break
1570
        else:
1571
          _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
1572
                      " show' disagrees", minor)
1573

    
1574
      if match_r and "local_dev" not in info:
1575
        # no local disk, but network attached and it matches
1576
        self._AssembleLocal(minor, self._children[0].dev_path,
1577
                            self._children[1].dev_path, self.size)
1578
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1579
          break
1580
        else:
1581
          _ThrowError("drbd%d: disk attach successful, but 'drbdsetup"
1582
                      " show' disagrees", minor)
1583

    
1584
      # this case must be considered only if we actually have local
1585
      # storage, i.e. not in diskless mode, because all diskless
1586
      # devices are equal from the point of view of local
1587
      # configuration
1588
      if (match_l and "local_dev" in info and
1589
          not match_r and "local_addr" in info):
1590
        # strange case - the device network part points to somewhere
1591
        # else, even though its local storage is ours; as we own the
1592
        # drbd space, we try to disconnect from the remote peer and
1593
        # reconnect to our correct one
1594
        try:
1595
          self._ShutdownNet(minor)
1596
        except errors.BlockDeviceError, err:
1597
          _ThrowError("drbd%d: device has correct local storage, wrong"
1598
                      " remote peer and is unable to disconnect in order"
1599
                      " to attach to the correct peer: %s", minor, str(err))
1600
        # note: _AssembleNet also handles the case when we don't want
1601
        # local storage (i.e. one or more of the _[lr](host|port) is
1602
        # None)
1603
        self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
1604
                          hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1605
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1606
          break
1607
        else:
1608
          _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
1609
                      " show' disagrees", minor)
1610

    
1611
    else:
1612
      minor = None
1613

    
1614
    self._SetFromMinor(minor)
1615
    if minor is None:
1616
      _ThrowError("drbd%d: cannot activate, unknown or unhandled reason",
1617
                  self._aminor)
1618

    
1619
  def _FastAssemble(self):
1620
    """Assemble the drbd device from zero.
1621

1622
    This is run when in Assemble we detect our minor is unused.
1623

1624
    """
1625
    minor = self._aminor
1626
    if self._children and self._children[0] and self._children[1]:
1627
      self._AssembleLocal(minor, self._children[0].dev_path,
1628
                          self._children[1].dev_path, self.size)
1629
    if self._lhost and self._lport and self._rhost and self._rport:
1630
      self._AssembleNet(minor,
1631
                        (self._lhost, self._lport, self._rhost, self._rport),
1632
                        constants.DRBD_NET_PROTOCOL,
1633
                        hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1634
    self._SetFromMinor(minor)
1635

    
1636
  @classmethod
1637
  def _ShutdownLocal(cls, minor):
1638
    """Detach from the local device.
1639

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

1643
    """
1644
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "detach"])
1645
    if result.failed:
1646
      _ThrowError("drbd%d: can't detach local disk: %s", minor, result.output)
1647

    
1648
  @classmethod
1649
  def _ShutdownNet(cls, minor):
1650
    """Disconnect from the remote peer.
1651

1652
    This fails if we don't have a local device.
1653

1654
    """
1655
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disconnect"])
1656
    if result.failed:
1657
      _ThrowError("drbd%d: can't shutdown network: %s", minor, result.output)
1658

    
1659
  @classmethod
1660
  def _ShutdownAll(cls, minor):
1661
    """Deactivate the device.
1662

1663
    This will, of course, fail if the device is in use.
1664

1665
    """
1666
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "down"])
1667
    if result.failed:
1668
      _ThrowError("drbd%d: can't shutdown drbd device: %s",
1669
                  minor, result.output)
1670

    
1671
  def Shutdown(self):
1672
    """Shutdown the DRBD device.
1673

1674
    """
1675
    if self.minor is None and not self.Attach():
1676
      logging.info("drbd%d: not attached during Shutdown()", self._aminor)
1677
      return
1678
    minor = self.minor
1679
    self.minor = None
1680
    self.dev_path = None
1681
    self._ShutdownAll(minor)
1682

    
1683
  def Remove(self):
1684
    """Stub remove for DRBD devices.
1685

1686
    """
1687
    self.Shutdown()
1688

    
1689
  @classmethod
1690
  def Create(cls, unique_id, children, size):
1691
    """Create a new DRBD8 device.
1692

1693
    Since DRBD devices are not created per se, just assembled, this
1694
    function only initializes the metadata.
1695

1696
    """
1697
    if len(children) != 2:
1698
      raise errors.ProgrammerError("Invalid setup for the drbd device")
1699
    # check that the minor is unused
1700
    aminor = unique_id[4]
1701
    proc_info = cls._MassageProcData(cls._GetProcData())
1702
    if aminor in proc_info:
1703
      status = DRBD8Status(proc_info[aminor])
1704
      in_use = status.is_in_use
1705
    else:
1706
      in_use = False
1707
    if in_use:
1708
      _ThrowError("drbd%d: minor is already in use at Create() time", aminor)
1709
    meta = children[1]
1710
    meta.Assemble()
1711
    if not meta.Attach():
1712
      _ThrowError("drbd%d: can't attach to meta device '%s'",
1713
                  aminor, meta)
1714
    cls._CheckMetaSize(meta.dev_path)
1715
    cls._InitMeta(aminor, meta.dev_path)
1716
    return cls(unique_id, children, size)
1717

    
1718
  def Grow(self, amount):
1719
    """Resize the DRBD device and its backing storage.
1720

1721
    """
1722
    if self.minor is None:
1723
      _ThrowError("drbd%d: Grow called while not attached", self._aminor)
1724
    if len(self._children) != 2 or None in self._children:
1725
      _ThrowError("drbd%d: cannot grow diskless device", self.minor)
1726
    self._children[0].Grow(amount)
1727
    result = utils.RunCmd(["drbdsetup", self.dev_path, "resize", "-s",
1728
                           "%dm" % (self.size + amount)])
1729
    if result.failed:
1730
      _ThrowError("drbd%d: resize failed: %s", self.minor, result.output)
1731

    
1732

    
1733
class FileStorage(BlockDev):
1734
  """File device.
1735

1736
  This class represents the a file storage backend device.
1737

1738
  The unique_id for the file device is a (file_driver, file_path) tuple.
1739

1740
  """
1741
  def __init__(self, unique_id, children, size):
1742
    """Initalizes a file device backend.
1743

1744
    """
1745
    if children:
1746
      raise errors.BlockDeviceError("Invalid setup for file device")
1747
    super(FileStorage, self).__init__(unique_id, children, size)
1748
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1749
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1750
    self.driver = unique_id[0]
1751
    self.dev_path = unique_id[1]
1752
    self.Attach()
1753

    
1754
  def Assemble(self):
1755
    """Assemble the device.
1756

1757
    Checks whether the file device exists, raises BlockDeviceError otherwise.
1758

1759
    """
1760
    if not os.path.exists(self.dev_path):
1761
      _ThrowError("File device '%s' does not exist" % self.dev_path)
1762

    
1763
  def Shutdown(self):
1764
    """Shutdown the device.
1765

1766
    This is a no-op for the file type, as we don't deactivate
1767
    the file on shutdown.
1768

1769
    """
1770
    pass
1771

    
1772
  def Open(self, force=False):
1773
    """Make the device ready for I/O.
1774

1775
    This is a no-op for the file type.
1776

1777
    """
1778
    pass
1779

    
1780
  def Close(self):
1781
    """Notifies that the device will no longer be used for I/O.
1782

1783
    This is a no-op for the file type.
1784

1785
    """
1786
    pass
1787

    
1788
  def Remove(self):
1789
    """Remove the file backing the block device.
1790

1791
    @rtype: boolean
1792
    @return: True if the removal was successful
1793

1794
    """
1795
    try:
1796
      os.remove(self.dev_path)
1797
    except OSError, err:
1798
      if err.errno != errno.ENOENT:
1799
        _ThrowError("Can't remove file '%s': %s", self.dev_path, err)
1800

    
1801
  def Attach(self):
1802
    """Attach to an existing file.
1803

1804
    Check if this file already exists.
1805

1806
    @rtype: boolean
1807
    @return: True if file exists
1808

1809
    """
1810
    self.attached = os.path.exists(self.dev_path)
1811
    return self.attached
1812

    
1813
  def GetActualSize(self):
1814
    """Return the actual disk size.
1815

1816
    @note: the device needs to be active when this is called
1817

1818
    """
1819
    assert self.attached, "BlockDevice not attached in GetActualSize()"
1820
    try:
1821
      st = os.stat(self.dev_path)
1822
      return st.st_size
1823
    except OSError, err:
1824
      _ThrowError("Can't stat %s: %s", self.dev_path, err)
1825

    
1826
  @classmethod
1827
  def Create(cls, unique_id, children, size):
1828
    """Create a new file.
1829

1830
    @param size: the size of file in MiB
1831

1832
    @rtype: L{bdev.FileStorage}
1833
    @return: an instance of FileStorage
1834

1835
    """
1836
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1837
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1838
    dev_path = unique_id[1]
1839
    if os.path.exists(dev_path):
1840
      _ThrowError("File already existing: %s", dev_path)
1841
    try:
1842
      f = open(dev_path, 'w')
1843
      f.truncate(size * 1024 * 1024)
1844
      f.close()
1845
    except IOError, err:
1846
      _ThrowError("Error in file creation: %", str(err))
1847

    
1848
    return FileStorage(unique_id, children, size)
1849

    
1850

    
1851
DEV_MAP = {
1852
  constants.LD_LV: LogicalVolume,
1853
  constants.LD_DRBD8: DRBD8,
1854
  constants.LD_FILE: FileStorage,
1855
  }
1856

    
1857

    
1858
def FindDevice(dev_type, unique_id, children, size):
1859
  """Search for an existing, assembled device.
1860

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

1864
  """
1865
  if dev_type not in DEV_MAP:
1866
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1867
  device = DEV_MAP[dev_type](unique_id, children, size)
1868
  if not device.attached:
1869
    return None
1870
  return device
1871

    
1872

    
1873
def Assemble(dev_type, unique_id, children, size):
1874
  """Try to attach or assemble an existing device.
1875

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

1879
  """
1880
  if dev_type not in DEV_MAP:
1881
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1882
  device = DEV_MAP[dev_type](unique_id, children, size)
1883
  device.Assemble()
1884
  return device
1885

    
1886

    
1887
def Create(dev_type, unique_id, children, size):
1888
  """Create a device.
1889

1890
  """
1891
  if dev_type not in DEV_MAP:
1892
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1893
  device = DEV_MAP[dev_type].Create(unique_id, children, size)
1894
  return device