Statistics
| Branch: | Tag: | Revision:

root / lib / bdev.py @ 73086975

History | View | Annotate | Download (59.8 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
    free_size = sum([ pv[0] for pv in pvs_info ])
366
    current_pvs = len(pvlist)
367
    stripes = min(current_pvs, constants.LVM_STRIPECOUNT)
368

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

    
388
  @staticmethod
389
  def GetPVInfo(vg_name):
390
    """Get the free space info for PVs in a volume group.
391

392
    @param vg_name: the volume group name
393

394
    @rtype: list
395
    @return: list of tuples (free_space, name) with free_space in mebibytes
396

397
    """
398
    command = ["pvs", "--noheadings", "--nosuffix", "--units=m",
399
               "-opv_name,vg_name,pv_free,pv_attr", "--unbuffered",
400
               "--separator=:"]
401
    result = utils.RunCmd(command)
402
    if result.failed:
403
      logging.error("Can't get the PV information: %s - %s",
404
                    result.fail_reason, result.output)
405
      return None
406
    data = []
407
    for line in result.stdout.splitlines():
408
      fields = line.strip().split(':')
409
      if len(fields) != 4:
410
        logging.error("Can't parse pvs output: line '%s'", line)
411
        return None
412
      # skip over pvs from another vg or ones which are not allocatable
413
      if fields[1] != vg_name or fields[3][0] != 'a':
414
        continue
415
      data.append((float(fields[2]), fields[0]))
416

    
417
    return data
418

    
419
  def Remove(self):
420
    """Remove this logical volume.
421

422
    """
423
    if not self.minor and not self.Attach():
424
      # the LV does not exist
425
      return
426
    result = utils.RunCmd(["lvremove", "-f", "%s/%s" %
427
                           (self._vg_name, self._lv_name)])
428
    if result.failed:
429
      _ThrowError("Can't lvremove: %s - %s", result.fail_reason, result.output)
430

    
431
  def Rename(self, new_id):
432
    """Rename this logical volume.
433

434
    """
435
    if not isinstance(new_id, (tuple, list)) or len(new_id) != 2:
436
      raise errors.ProgrammerError("Invalid new logical id '%s'" % new_id)
437
    new_vg, new_name = new_id
438
    if new_vg != self._vg_name:
439
      raise errors.ProgrammerError("Can't move a logical volume across"
440
                                   " volume groups (from %s to to %s)" %
441
                                   (self._vg_name, new_vg))
442
    result = utils.RunCmd(["lvrename", new_vg, self._lv_name, new_name])
443
    if result.failed:
444
      _ThrowError("Failed to rename the logical volume: %s", result.output)
445
    self._lv_name = new_name
446
    self.dev_path = "/dev/%s/%s" % (self._vg_name, self._lv_name)
447

    
448
  def Attach(self):
449
    """Attach to an existing LV.
450

451
    This method will try to see if an existing and active LV exists
452
    which matches our name. If so, its major/minor will be
453
    recorded.
454

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

    
481
    status, major, minor, pe_size, stripes = out
482
    if len(status) != 6:
483
      logging.error("lvs lv_attr is not 6 characters (%s)", status)
484
      return False
485

    
486
    try:
487
      major = int(major)
488
      minor = int(minor)
489
    except ValueError, err:
490
      logging.error("lvs major/minor cannot be parsed: %s", str(err))
491

    
492
    try:
493
      pe_size = int(float(pe_size))
494
    except (TypeError, ValueError), err:
495
      logging.error("Can't parse vg extent size: %s", err)
496
      return False
497

    
498
    try:
499
      stripes = int(stripes)
500
    except (TypeError, ValueError), err:
501
      logging.error("Can't parse the number of stripes: %s", err)
502
      return False
503

    
504
    self.major = major
505
    self.minor = minor
506
    self.pe_size = pe_size
507
    self.stripe_count = stripes
508
    self._degraded = status[0] == 'v' # virtual volume, i.e. doesn't backing
509
                                      # storage
510
    self.attached = True
511
    return True
512

    
513
  def Assemble(self):
514
    """Assemble the device.
515

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

520
    """
521
    result = utils.RunCmd(["lvchange", "-ay", self.dev_path])
522
    if result.failed:
523
      _ThrowError("Can't activate lv %s: %s", self.dev_path, result.output)
524

    
525
  def Shutdown(self):
526
    """Shutdown the device.
527

528
    This is a no-op for the LV device type, as we don't deactivate the
529
    volumes on shutdown.
530

531
    """
532
    pass
533

    
534
  def GetSyncStatus(self):
535
    """Returns the sync status of the device.
536

537
    If this device is a mirroring device, this function returns the
538
    status of the mirror.
539

540
    For logical volumes, sync_percent and estimated_time are always
541
    None (no recovery in progress, as we don't handle the mirrored LV
542
    case). The is_degraded parameter is the inverse of the ldisk
543
    parameter.
544

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

551
    The status was already read in Attach, so we just return it.
552

553
    @rtype: objects.BlockDevStatus
554

555
    """
556
    if self._degraded:
557
      ldisk_status = constants.LDS_FAULTY
558
    else:
559
      ldisk_status = constants.LDS_OKAY
560

    
561
    return objects.BlockDevStatus(dev_path=self.dev_path,
562
                                  major=self.major,
563
                                  minor=self.minor,
564
                                  sync_percent=None,
565
                                  estimated_time=None,
566
                                  is_degraded=self._degraded,
567
                                  ldisk_status=ldisk_status)
568

    
569
  def Open(self, force=False):
570
    """Make the device ready for I/O.
571

572
    This is a no-op for the LV device type.
573

574
    """
575
    pass
576

    
577
  def Close(self):
578
    """Notifies that the device will no longer be used for I/O.
579

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

582
    """
583
    pass
584

    
585
  def Snapshot(self, size):
586
    """Create a snapshot copy of an lvm block device.
587

588
    """
589
    snap_name = self._lv_name + ".snap"
590

    
591
    # remove existing snapshot if found
592
    snap = LogicalVolume((self._vg_name, snap_name), None, size)
593
    _IgnoreError(snap.Remove)
594

    
595
    pvs_info = self.GetPVInfo(self._vg_name)
596
    if not pvs_info:
597
      _ThrowError("Can't compute PV info for vg %s", self._vg_name)
598
    pvs_info.sort()
599
    pvs_info.reverse()
600
    free_size, pv_name = pvs_info[0]
601
    if free_size < size:
602
      _ThrowError("Not enough free space: required %s,"
603
                  " available %s", size, free_size)
604

    
605
    result = utils.RunCmd(["lvcreate", "-L%dm" % size, "-s",
606
                           "-n%s" % snap_name, self.dev_path])
607
    if result.failed:
608
      _ThrowError("command: %s error: %s - %s",
609
                  result.cmd, result.fail_reason, result.output)
610

    
611
    return snap_name
612

    
613
  def SetInfo(self, text):
614
    """Update metadata with info text.
615

616
    """
617
    BlockDev.SetInfo(self, text)
618

    
619
    # Replace invalid characters
620
    text = re.sub('^[^A-Za-z0-9_+.]', '_', text)
621
    text = re.sub('[^-A-Za-z0-9_+.]', '_', text)
622

    
623
    # Only up to 128 characters are allowed
624
    text = text[:128]
625

    
626
    result = utils.RunCmd(["lvchange", "--addtag", text,
627
                           self.dev_path])
628
    if result.failed:
629
      _ThrowError("Command: %s error: %s - %s", result.cmd, result.fail_reason,
630
                  result.output)
631

    
632
  def Grow(self, amount):
633
    """Grow the logical volume.
634

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

    
654

    
655
class DRBD8Status(object):
656
  """A DRBD status representation class.
657

658
  Note that this doesn't support unconfigured devices (cs:Unconfigured).
659

660
  """
661
  UNCONF_RE = re.compile(r"\s*[0-9]+:\s*cs:Unconfigured$")
662
  LINE_RE = re.compile(r"\s*[0-9]+:\s*cs:(\S+)\s+(?:st|ro):([^/]+)/(\S+)"
663
                       "\s+ds:([^/]+)/(\S+)\s+.*$")
664
  SYNC_RE = re.compile(r"^.*\ssync'ed:\s*([0-9.]+)%.*"
665
                       "\sfinish: ([0-9]+):([0-9]+):([0-9]+)\s.*$")
666

    
667
  CS_UNCONFIGURED = "Unconfigured"
668
  CS_STANDALONE = "StandAlone"
669
  CS_WFCONNECTION = "WFConnection"
670
  CS_WFREPORTPARAMS = "WFReportParams"
671
  CS_CONNECTED = "Connected"
672
  CS_STARTINGSYNCS = "StartingSyncS"
673
  CS_STARTINGSYNCT = "StartingSyncT"
674
  CS_WFBITMAPS = "WFBitMapS"
675
  CS_WFBITMAPT = "WFBitMapT"
676
  CS_WFSYNCUUID = "WFSyncUUID"
677
  CS_SYNCSOURCE = "SyncSource"
678
  CS_SYNCTARGET = "SyncTarget"
679
  CS_PAUSEDSYNCS = "PausedSyncS"
680
  CS_PAUSEDSYNCT = "PausedSyncT"
681
  CSET_SYNC = frozenset([
682
    CS_WFREPORTPARAMS,
683
    CS_STARTINGSYNCS,
684
    CS_STARTINGSYNCT,
685
    CS_WFBITMAPS,
686
    CS_WFBITMAPT,
687
    CS_WFSYNCUUID,
688
    CS_SYNCSOURCE,
689
    CS_SYNCTARGET,
690
    CS_PAUSEDSYNCS,
691
    CS_PAUSEDSYNCT,
692
    ])
693

    
694
  DS_DISKLESS = "Diskless"
695
  DS_ATTACHING = "Attaching" # transient state
696
  DS_FAILED = "Failed" # transient state, next: diskless
697
  DS_NEGOTIATING = "Negotiating" # transient state
698
  DS_INCONSISTENT = "Inconsistent" # while syncing or after creation
699
  DS_OUTDATED = "Outdated"
700
  DS_DUNKNOWN = "DUnknown" # shown for peer disk when not connected
701
  DS_CONSISTENT = "Consistent"
702
  DS_UPTODATE = "UpToDate" # normal state
703

    
704
  RO_PRIMARY = "Primary"
705
  RO_SECONDARY = "Secondary"
706
  RO_UNKNOWN = "Unknown"
707

    
708
  def __init__(self, procline):
709
    u = self.UNCONF_RE.match(procline)
710
    if u:
711
      self.cstatus = self.CS_UNCONFIGURED
712
      self.lrole = self.rrole = self.ldisk = self.rdisk = None
713
    else:
714
      m = self.LINE_RE.match(procline)
715
      if not m:
716
        raise errors.BlockDeviceError("Can't parse input data '%s'" % procline)
717
      self.cstatus = m.group(1)
718
      self.lrole = m.group(2)
719
      self.rrole = m.group(3)
720
      self.ldisk = m.group(4)
721
      self.rdisk = m.group(5)
722

    
723
    # end reading of data from the LINE_RE or UNCONF_RE
724

    
725
    self.is_standalone = self.cstatus == self.CS_STANDALONE
726
    self.is_wfconn = self.cstatus == self.CS_WFCONNECTION
727
    self.is_connected = self.cstatus == self.CS_CONNECTED
728
    self.is_primary = self.lrole == self.RO_PRIMARY
729
    self.is_secondary = self.lrole == self.RO_SECONDARY
730
    self.peer_primary = self.rrole == self.RO_PRIMARY
731
    self.peer_secondary = self.rrole == self.RO_SECONDARY
732
    self.both_primary = self.is_primary and self.peer_primary
733
    self.both_secondary = self.is_secondary and self.peer_secondary
734

    
735
    self.is_diskless = self.ldisk == self.DS_DISKLESS
736
    self.is_disk_uptodate = self.ldisk == self.DS_UPTODATE
737

    
738
    self.is_in_resync = self.cstatus in self.CSET_SYNC
739
    self.is_in_use = self.cstatus != self.CS_UNCONFIGURED
740

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

    
759

    
760
class BaseDRBD(BlockDev):
761
  """Base DRBD class.
762

763
  This class contains a few bits of common functionality between the
764
  0.7 and 8.x versions of DRBD.
765

766
  """
767
  _VERSION_RE = re.compile(r"^version: (\d+)\.(\d+)\.(\d+)"
768
                           r" \(api:(\d+)/proto:(\d+)(?:-(\d+))?\)")
769

    
770
  _DRBD_MAJOR = 147
771
  _ST_UNCONFIGURED = "Unconfigured"
772
  _ST_WFCONNECTION = "WFConnection"
773
  _ST_CONNECTED = "Connected"
774

    
775
  _STATUS_FILE = "/proc/drbd"
776

    
777
  @staticmethod
778
  def _GetProcData(filename=_STATUS_FILE):
779
    """Return data from /proc/drbd.
780

781
    """
782
    try:
783
      stat = open(filename, "r")
784
      try:
785
        data = stat.read().splitlines()
786
      finally:
787
        stat.close()
788
    except EnvironmentError, err:
789
      if err.errno == errno.ENOENT:
790
        _ThrowError("The file %s cannot be opened, check if the module"
791
                    " is loaded (%s)", filename, str(err))
792
      else:
793
        _ThrowError("Can't read the DRBD proc file %s: %s", filename, str(err))
794
    if not data:
795
      _ThrowError("Can't read any data from %s", filename)
796
    return data
797

    
798
  @staticmethod
799
  def _MassageProcData(data):
800
    """Transform the output of _GetProdData into a nicer form.
801

802
    @return: a dictionary of minor: joined lines from /proc/drbd
803
        for that minor
804

805
    """
806
    lmatch = re.compile("^ *([0-9]+):.*$")
807
    results = {}
808
    old_minor = old_line = None
809
    for line in data:
810
      lresult = lmatch.match(line)
811
      if lresult is not None:
812
        if old_minor is not None:
813
          results[old_minor] = old_line
814
        old_minor = int(lresult.group(1))
815
        old_line = line
816
      else:
817
        if old_minor is not None:
818
          old_line += " " + line.strip()
819
    # add last line
820
    if old_minor is not None:
821
      results[old_minor] = old_line
822
    return results
823

    
824
  @classmethod
825
  def _GetVersion(cls):
826
    """Return the DRBD version.
827

828
    This will return a dict with keys:
829
      - k_major
830
      - k_minor
831
      - k_point
832
      - api
833
      - proto
834
      - proto2 (only on drbd > 8.2.X)
835

836
    """
837
    proc_data = cls._GetProcData()
838
    first_line = proc_data[0].strip()
839
    version = cls._VERSION_RE.match(first_line)
840
    if not version:
841
      raise errors.BlockDeviceError("Can't parse DRBD version from '%s'" %
842
                                    first_line)
843

    
844
    values = version.groups()
845
    retval = {'k_major': int(values[0]),
846
              'k_minor': int(values[1]),
847
              'k_point': int(values[2]),
848
              'api': int(values[3]),
849
              'proto': int(values[4]),
850
             }
851
    if values[5] is not None:
852
      retval['proto2'] = values[5]
853

    
854
    return retval
855

    
856
  @staticmethod
857
  def _DevPath(minor):
858
    """Return the path to a drbd device for a given minor.
859

860
    """
861
    return "/dev/drbd%d" % minor
862

    
863
  @classmethod
864
  def GetUsedDevs(cls):
865
    """Compute the list of used DRBD devices.
866

867
    """
868
    data = cls._GetProcData()
869

    
870
    used_devs = {}
871
    valid_line = re.compile("^ *([0-9]+): cs:([^ ]+).*$")
872
    for line in data:
873
      match = valid_line.match(line)
874
      if not match:
875
        continue
876
      minor = int(match.group(1))
877
      state = match.group(2)
878
      if state == cls._ST_UNCONFIGURED:
879
        continue
880
      used_devs[minor] = state, line
881

    
882
    return used_devs
883

    
884
  def _SetFromMinor(self, minor):
885
    """Set our parameters based on the given minor.
886

887
    This sets our minor variable and our dev_path.
888

889
    """
890
    if minor is None:
891
      self.minor = self.dev_path = None
892
      self.attached = False
893
    else:
894
      self.minor = minor
895
      self.dev_path = self._DevPath(minor)
896
      self.attached = True
897

    
898
  @staticmethod
899
  def _CheckMetaSize(meta_device):
900
    """Check if the given meta device looks like a valid one.
901

902
    This currently only check the size, which must be around
903
    128MiB.
904

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

    
926
  def Rename(self, new_id):
927
    """Rename a device.
928

929
    This is not supported for drbd devices.
930

931
    """
932
    raise errors.ProgrammerError("Can't rename a drbd device")
933

    
934

    
935
class DRBD8(BaseDRBD):
936
  """DRBD v8.x block device.
937

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

942
  The unique_id for the drbd device is the (local_ip, local_port,
943
  remote_ip, remote_port) tuple, and it must have two children: the
944
  data device and the meta_device. The meta device is checked for
945
  valid size and is zeroed on create.
946

947
  """
948
  _MAX_MINORS = 255
949
  _PARSE_SHOW = None
950

    
951
  # timeout constants
952
  _NET_RECONFIG_TIMEOUT = 60
953

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

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

    
978
  @classmethod
979
  def _InitMeta(cls, minor, dev_path):
980
    """Initialize a meta device.
981

982
    This will not work if the given minor is in use.
983

984
    """
985
    result = utils.RunCmd(["drbdmeta", "--force", cls._DevPath(minor),
986
                           "v08", dev_path, "0", "create-md"])
987
    if result.failed:
988
      _ThrowError("Can't initialize meta device: %s", result.output)
989

    
990
  @classmethod
991
  def _FindUnusedMinor(cls):
992
    """Find an unused DRBD device.
993

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

997
    """
998
    data = cls._GetProcData()
999

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

    
1018
  @classmethod
1019
  def _GetShowParser(cls):
1020
    """Return a parser for `drbd show` output.
1021

1022
    This will either create or return an already-create parser for the
1023
    output of the command `drbd show`.
1024

1025
    """
1026
    if cls._PARSE_SHOW is not None:
1027
      return cls._PARSE_SHOW
1028

    
1029
    # pyparsing setup
1030
    lbrace = pyp.Literal("{").suppress()
1031
    rbrace = pyp.Literal("}").suppress()
1032
    semi = pyp.Literal(";").suppress()
1033
    # this also converts the value to an int
1034
    number = pyp.Word(pyp.nums).setParseAction(lambda s, l, t: int(t[0]))
1035

    
1036
    comment = pyp.Literal ("#") + pyp.Optional(pyp.restOfLine)
1037
    defa = pyp.Literal("_is_default").suppress()
1038
    dbl_quote = pyp.Literal('"').suppress()
1039

    
1040
    keyword = pyp.Word(pyp.alphanums + '-')
1041

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

    
1055
    # a statement
1056
    stmt = (~rbrace + keyword + ~lbrace +
1057
            pyp.Optional(addr_port ^ value ^ quoted ^ meta_value ^
1058
                         device_value) +
1059
            pyp.Optional(defa) + semi +
1060
            pyp.Optional(pyp.restOfLine).suppress())
1061

    
1062
    # an entire section
1063
    section_name = pyp.Word(pyp.alphas + '_')
1064
    section = section_name + lbrace + pyp.ZeroOrMore(pyp.Group(stmt)) + rbrace
1065

    
1066
    bnf = pyp.ZeroOrMore(pyp.Group(section ^ stmt))
1067
    bnf.ignore(comment)
1068

    
1069
    cls._PARSE_SHOW = bnf
1070

    
1071
    return bnf
1072

    
1073
  @classmethod
1074
  def _GetShowData(cls, minor):
1075
    """Return the `drbdsetup show` data for a minor.
1076

1077
    """
1078
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "show"])
1079
    if result.failed:
1080
      logging.error("Can't display the drbd config: %s - %s",
1081
                    result.fail_reason, result.output)
1082
      return None
1083
    return result.stdout
1084

    
1085
  @classmethod
1086
  def _GetDevInfo(cls, out):
1087
    """Parse details about a given DRBD minor.
1088

1089
    This return, if available, the local backing device (as a path)
1090
    and the local and remote (ip, port) information from a string
1091
    containing the output of the `drbdsetup show` command as returned
1092
    by _GetShowData.
1093

1094
    """
1095
    data = {}
1096
    if not out:
1097
      return data
1098

    
1099
    bnf = cls._GetShowParser()
1100
    # run pyparse
1101

    
1102
    try:
1103
      results = bnf.parseString(out)
1104
    except pyp.ParseException, err:
1105
      _ThrowError("Can't parse drbdsetup show output: %s", str(err))
1106

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

    
1125
  def _MatchesLocal(self, info):
1126
    """Test if our local config matches with an existing device.
1127

1128
    The parameter should be as returned from `_GetDevInfo()`. This
1129
    method tests if our local backing device is the same as the one in
1130
    the info parameter, in effect testing if we look like the given
1131
    device.
1132

1133
    """
1134
    if self._children:
1135
      backend, meta = self._children
1136
    else:
1137
      backend = meta = None
1138

    
1139
    if backend is not None:
1140
      retval = ("local_dev" in info and info["local_dev"] == backend.dev_path)
1141
    else:
1142
      retval = ("local_dev" not in info)
1143

    
1144
    if meta is not None:
1145
      retval = retval and ("meta_dev" in info and
1146
                           info["meta_dev"] == meta.dev_path)
1147
      retval = retval and ("meta_index" in info and
1148
                           info["meta_index"] == 0)
1149
    else:
1150
      retval = retval and ("meta_dev" not in info and
1151
                           "meta_index" not in info)
1152
    return retval
1153

    
1154
  def _MatchesNet(self, info):
1155
    """Test if our network config matches with an existing device.
1156

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

1162
    """
1163
    if (((self._lhost is None and not ("local_addr" in info)) and
1164
         (self._rhost is None and not ("remote_addr" in info)))):
1165
      return True
1166

    
1167
    if self._lhost is None:
1168
      return False
1169

    
1170
    if not ("local_addr" in info and
1171
            "remote_addr" in info):
1172
      return False
1173

    
1174
    retval = (info["local_addr"] == (self._lhost, self._lport))
1175
    retval = (retval and
1176
              info["remote_addr"] == (self._rhost, self._rport))
1177
    return retval
1178

    
1179
  @classmethod
1180
  def _AssembleLocal(cls, minor, backend, meta, size):
1181
    """Configure the local part of a DRBD device.
1182

1183
    """
1184
    args = ["drbdsetup", cls._DevPath(minor), "disk",
1185
            backend, meta, "0",
1186
            "-e", "detach",
1187
            "--create-device"]
1188
    if size:
1189
      args.extend(["-d", "%sm" % size])
1190
    result = utils.RunCmd(args)
1191
    if result.failed:
1192
      _ThrowError("drbd%d: can't attach local disk: %s", minor, result.output)
1193

    
1194
  @classmethod
1195
  def _AssembleNet(cls, minor, net_info, protocol,
1196
                   dual_pri=False, hmac=None, secret=None):
1197
    """Configure the network part of the device.
1198

1199
    """
1200
    lhost, lport, rhost, rport = net_info
1201
    if None in net_info:
1202
      # we don't want network connection and actually want to make
1203
      # sure its shutdown
1204
      cls._ShutdownNet(minor)
1205
      return
1206

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

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

    
1230
    timeout = time.time() + 10
1231
    ok = False
1232
    while time.time() < timeout:
1233
      info = cls._GetDevInfo(cls._GetShowData(minor))
1234
      if not "local_addr" in info or not "remote_addr" in info:
1235
        time.sleep(1)
1236
        continue
1237
      if (info["local_addr"] != (lhost, lport) or
1238
          info["remote_addr"] != (rhost, rport)):
1239
        time.sleep(1)
1240
        continue
1241
      ok = True
1242
      break
1243
    if not ok:
1244
      _ThrowError("drbd%d: timeout while configuring network", minor)
1245

    
1246
  def AddChildren(self, devices):
1247
    """Add a disk to the DRBD device.
1248

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

    
1266
    self._AssembleLocal(self.minor, backend.dev_path, meta.dev_path, self.size)
1267
    self._children = devices
1268

    
1269
  def RemoveChildren(self, devices):
1270
    """Detach the drbd device from local storage.
1271

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

    
1293
    self._ShutdownLocal(self.minor)
1294
    self._children = []
1295

    
1296
  @classmethod
1297
  def _SetMinorSyncSpeed(cls, minor, kbytes):
1298
    """Set the speed of the DRBD syncer.
1299

1300
    This is the low-level implementation.
1301

1302
    @type minor: int
1303
    @param minor: the drbd minor whose settings we change
1304
    @type kbytes: int
1305
    @param kbytes: the speed in kbytes/second
1306
    @rtype: boolean
1307
    @return: the success of the operation
1308

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

    
1317
  def SetSyncSpeed(self, kbytes):
1318
    """Set the speed of the DRBD syncer.
1319

1320
    @type kbytes: int
1321
    @param kbytes: the speed in kbytes/second
1322
    @rtype: boolean
1323
    @return: the success of the operation
1324

1325
    """
1326
    if self.minor is None:
1327
      logging.info("Not attached during SetSyncSpeed")
1328
      return False
1329
    children_result = super(DRBD8, self).SetSyncSpeed(kbytes)
1330
    return self._SetMinorSyncSpeed(self.minor, kbytes) and children_result
1331

    
1332
  def GetProcStatus(self):
1333
    """Return device data from /proc.
1334

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

    
1343
  def GetSyncStatus(self):
1344
    """Returns the sync status of the device.
1345

1346

1347
    If sync_percent is None, it means all is ok
1348
    If estimated_time is None, it means we can't estimate
1349
    the time needed, otherwise it's the time left in seconds.
1350

1351

1352
    We set the is_degraded parameter to True on two conditions:
1353
    network not connected or local disk missing.
1354

1355
    We compute the ldisk parameter based on whether we have a local
1356
    disk or not.
1357

1358
    @rtype: objects.BlockDevStatus
1359

1360
    """
1361
    if self.minor is None and not self.Attach():
1362
      _ThrowError("drbd%d: can't Attach() in GetSyncStatus", self._aminor)
1363

    
1364
    stats = self.GetProcStatus()
1365
    is_degraded = not stats.is_connected or not stats.is_disk_uptodate
1366

    
1367
    if stats.is_disk_uptodate:
1368
      ldisk_status = constants.LDS_OKAY
1369
    elif stats.is_diskless:
1370
      ldisk_status = constants.LDS_FAULTY
1371
    else:
1372
      ldisk_status = constants.LDS_UNKNOWN
1373

    
1374
    return objects.BlockDevStatus(dev_path=self.dev_path,
1375
                                  major=self.major,
1376
                                  minor=self.minor,
1377
                                  sync_percent=stats.sync_percent,
1378
                                  estimated_time=stats.est_time,
1379
                                  is_degraded=is_degraded,
1380
                                  ldisk_status=ldisk_status)
1381

    
1382
  def Open(self, force=False):
1383
    """Make the local state primary.
1384

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

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

    
1402
  def Close(self):
1403
    """Make the local state secondary.
1404

1405
    This will, of course, fail if the device is in use.
1406

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

    
1415
  def DisconnectNet(self):
1416
    """Removes network configuration.
1417

1418
    This method shutdowns the network side of the device.
1419

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

1428
    """
1429
    if self.minor is None:
1430
      _ThrowError("drbd%d: disk not attached in re-attach net", self._aminor)
1431

    
1432
    if None in (self._lhost, self._lport, self._rhost, self._rport):
1433
      _ThrowError("drbd%d: DRBD disk missing network info in"
1434
                  " DisconnectNet()", self.minor)
1435

    
1436
    ever_disconnected = _IgnoreError(self._ShutdownNet, self.minor)
1437
    timeout_limit = time.time() + self._NET_RECONFIG_TIMEOUT
1438
    sleep_time = 0.100 # we start the retry time at 100 milliseconds
1439
    while time.time() < timeout_limit:
1440
      status = self.GetProcStatus()
1441
      if status.is_standalone:
1442
        break
1443
      # retry the disconnect, it seems possible that due to a
1444
      # well-time disconnect on the peer, my disconnect command might
1445
      # be ignored and forgotten
1446
      ever_disconnected = _IgnoreError(self._ShutdownNet, self.minor) or \
1447
                          ever_disconnected
1448
      time.sleep(sleep_time)
1449
      sleep_time = min(2, sleep_time * 1.5)
1450

    
1451
    if not status.is_standalone:
1452
      if ever_disconnected:
1453
        msg = ("drbd%d: device did not react to the"
1454
               " 'disconnect' command in a timely manner")
1455
      else:
1456
        msg = "drbd%d: can't shutdown network, even after multiple retries"
1457
      _ThrowError(msg, self.minor)
1458

    
1459
    reconfig_time = time.time() - timeout_limit + self._NET_RECONFIG_TIMEOUT
1460
    if reconfig_time > 15: # hardcoded alert limit
1461
      logging.info("drbd%d: DisconnectNet: detach took %.3f seconds",
1462
                   self.minor, reconfig_time)
1463

    
1464
  def AttachNet(self, multimaster):
1465
    """Reconnects the network.
1466

1467
    This method connects the network side of the device with a
1468
    specified multi-master flag. The device needs to be 'Standalone'
1469
    but have valid network configuration data.
1470

1471
    Args:
1472
      - multimaster: init the network in dual-primary mode
1473

1474
    """
1475
    if self.minor is None:
1476
      _ThrowError("drbd%d: device not attached in AttachNet", self._aminor)
1477

    
1478
    if None in (self._lhost, self._lport, self._rhost, self._rport):
1479
      _ThrowError("drbd%d: missing network info in AttachNet()", self.minor)
1480

    
1481
    status = self.GetProcStatus()
1482

    
1483
    if not status.is_standalone:
1484
      _ThrowError("drbd%d: device is not standalone in AttachNet", self.minor)
1485

    
1486
    self._AssembleNet(self.minor,
1487
                      (self._lhost, self._lport, self._rhost, self._rport),
1488
                      constants.DRBD_NET_PROTOCOL, dual_pri=multimaster,
1489
                      hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1490

    
1491
  def Attach(self):
1492
    """Check if our minor is configured.
1493

1494
    This doesn't do any device configurations - it only checks if the
1495
    minor is in a state different from Unconfigured.
1496

1497
    Note that this function will not change the state of the system in
1498
    any way (except in case of side-effects caused by reading from
1499
    /proc).
1500

1501
    """
1502
    used_devs = self.GetUsedDevs()
1503
    if self._aminor in used_devs:
1504
      minor = self._aminor
1505
    else:
1506
      minor = None
1507

    
1508
    self._SetFromMinor(minor)
1509
    return minor is not None
1510

    
1511
  def Assemble(self):
1512
    """Assemble the drbd.
1513

1514
    Method:
1515
      - if we have a configured device, we try to ensure that it matches
1516
        our config
1517
      - if not, we create it from zero
1518

1519
    """
1520
    super(DRBD8, self).Assemble()
1521

    
1522
    self.Attach()
1523
    if self.minor is None:
1524
      # local device completely unconfigured
1525
      self._FastAssemble()
1526
    else:
1527
      # we have to recheck the local and network status and try to fix
1528
      # the device
1529
      self._SlowAssemble()
1530

    
1531
  def _SlowAssemble(self):
1532
    """Assembles the DRBD device from a (partially) configured device.
1533

1534
    In case of partially attached (local device matches but no network
1535
    setup), we perform the network attach. If successful, we re-test
1536
    the attach if can return success.
1537

1538
    """
1539
    net_data = (self._lhost, self._lport, self._rhost, self._rport)
1540
    for minor in (self._aminor,):
1541
      info = self._GetDevInfo(self._GetShowData(minor))
1542
      match_l = self._MatchesLocal(info)
1543
      match_r = self._MatchesNet(info)
1544

    
1545
      if match_l and match_r:
1546
        # everything matches
1547
        break
1548

    
1549
      if match_l and not match_r and "local_addr" not in info:
1550
        # disk matches, but not attached to network, attach and recheck
1551
        self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
1552
                          hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1553
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1554
          break
1555
        else:
1556
          _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
1557
                      " show' disagrees", minor)
1558

    
1559
      if match_r and "local_dev" not in info:
1560
        # no local disk, but network attached and it matches
1561
        self._AssembleLocal(minor, self._children[0].dev_path,
1562
                            self._children[1].dev_path, self.size)
1563
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1564
          break
1565
        else:
1566
          _ThrowError("drbd%d: disk attach successful, but 'drbdsetup"
1567
                      " show' disagrees", minor)
1568

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

    
1596
    else:
1597
      minor = None
1598

    
1599
    self._SetFromMinor(minor)
1600
    if minor is None:
1601
      _ThrowError("drbd%d: cannot activate, unknown or unhandled reason",
1602
                  self._aminor)
1603

    
1604
  def _FastAssemble(self):
1605
    """Assemble the drbd device from zero.
1606

1607
    This is run when in Assemble we detect our minor is unused.
1608

1609
    """
1610
    minor = self._aminor
1611
    if self._children and self._children[0] and self._children[1]:
1612
      self._AssembleLocal(minor, self._children[0].dev_path,
1613
                          self._children[1].dev_path, self.size)
1614
    if self._lhost and self._lport and self._rhost and self._rport:
1615
      self._AssembleNet(minor,
1616
                        (self._lhost, self._lport, self._rhost, self._rport),
1617
                        constants.DRBD_NET_PROTOCOL,
1618
                        hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1619
    self._SetFromMinor(minor)
1620

    
1621
  @classmethod
1622
  def _ShutdownLocal(cls, minor):
1623
    """Detach from the local device.
1624

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

1628
    """
1629
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "detach"])
1630
    if result.failed:
1631
      _ThrowError("drbd%d: can't detach local disk: %s", minor, result.output)
1632

    
1633
  @classmethod
1634
  def _ShutdownNet(cls, minor):
1635
    """Disconnect from the remote peer.
1636

1637
    This fails if we don't have a local device.
1638

1639
    """
1640
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disconnect"])
1641
    if result.failed:
1642
      _ThrowError("drbd%d: can't shutdown network: %s", minor, result.output)
1643

    
1644
  @classmethod
1645
  def _ShutdownAll(cls, minor):
1646
    """Deactivate the device.
1647

1648
    This will, of course, fail if the device is in use.
1649

1650
    """
1651
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "down"])
1652
    if result.failed:
1653
      _ThrowError("drbd%d: can't shutdown drbd device: %s",
1654
                  minor, result.output)
1655

    
1656
  def Shutdown(self):
1657
    """Shutdown the DRBD device.
1658

1659
    """
1660
    if self.minor is None and not self.Attach():
1661
      logging.info("drbd%d: not attached during Shutdown()", self._aminor)
1662
      return
1663
    minor = self.minor
1664
    self.minor = None
1665
    self.dev_path = None
1666
    self._ShutdownAll(minor)
1667

    
1668
  def Remove(self):
1669
    """Stub remove for DRBD devices.
1670

1671
    """
1672
    self.Shutdown()
1673

    
1674
  @classmethod
1675
  def Create(cls, unique_id, children, size):
1676
    """Create a new DRBD8 device.
1677

1678
    Since DRBD devices are not created per se, just assembled, this
1679
    function only initializes the metadata.
1680

1681
    """
1682
    if len(children) != 2:
1683
      raise errors.ProgrammerError("Invalid setup for the drbd device")
1684
    # check that the minor is unused
1685
    aminor = unique_id[4]
1686
    proc_info = cls._MassageProcData(cls._GetProcData())
1687
    if aminor in proc_info:
1688
      status = DRBD8Status(proc_info[aminor])
1689
      in_use = status.is_in_use
1690
    else:
1691
      in_use = False
1692
    if in_use:
1693
      _ThrowError("drbd%d: minor is already in use at Create() time", aminor)
1694
    meta = children[1]
1695
    meta.Assemble()
1696
    if not meta.Attach():
1697
      _ThrowError("drbd%d: can't attach to meta device '%s'",
1698
                  aminor, meta)
1699
    cls._CheckMetaSize(meta.dev_path)
1700
    cls._InitMeta(aminor, meta.dev_path)
1701
    return cls(unique_id, children, size)
1702

    
1703
  def Grow(self, amount):
1704
    """Resize the DRBD device and its backing storage.
1705

1706
    """
1707
    if self.minor is None:
1708
      _ThrowError("drbd%d: Grow called while not attached", self._aminor)
1709
    if len(self._children) != 2 or None in self._children:
1710
      _ThrowError("drbd%d: cannot grow diskless device", self.minor)
1711
    self._children[0].Grow(amount)
1712
    result = utils.RunCmd(["drbdsetup", self.dev_path, "resize", "-s",
1713
                           "%dm" % (self.size + amount)])
1714
    if result.failed:
1715
      _ThrowError("drbd%d: resize failed: %s", self.minor, result.output)
1716

    
1717

    
1718
class FileStorage(BlockDev):
1719
  """File device.
1720

1721
  This class represents the a file storage backend device.
1722

1723
  The unique_id for the file device is a (file_driver, file_path) tuple.
1724

1725
  """
1726
  def __init__(self, unique_id, children, size):
1727
    """Initalizes a file device backend.
1728

1729
    """
1730
    if children:
1731
      raise errors.BlockDeviceError("Invalid setup for file device")
1732
    super(FileStorage, self).__init__(unique_id, children, size)
1733
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1734
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1735
    self.driver = unique_id[0]
1736
    self.dev_path = unique_id[1]
1737
    self.Attach()
1738

    
1739
  def Assemble(self):
1740
    """Assemble the device.
1741

1742
    Checks whether the file device exists, raises BlockDeviceError otherwise.
1743

1744
    """
1745
    if not os.path.exists(self.dev_path):
1746
      _ThrowError("File device '%s' does not exist" % self.dev_path)
1747

    
1748
  def Shutdown(self):
1749
    """Shutdown the device.
1750

1751
    This is a no-op for the file type, as we don't deactivate
1752
    the file on shutdown.
1753

1754
    """
1755
    pass
1756

    
1757
  def Open(self, force=False):
1758
    """Make the device ready for I/O.
1759

1760
    This is a no-op for the file type.
1761

1762
    """
1763
    pass
1764

    
1765
  def Close(self):
1766
    """Notifies that the device will no longer be used for I/O.
1767

1768
    This is a no-op for the file type.
1769

1770
    """
1771
    pass
1772

    
1773
  def Remove(self):
1774
    """Remove the file backing the block device.
1775

1776
    @rtype: boolean
1777
    @return: True if the removal was successful
1778

1779
    """
1780
    try:
1781
      os.remove(self.dev_path)
1782
    except OSError, err:
1783
      if err.errno != errno.ENOENT:
1784
        _ThrowError("Can't remove file '%s': %s", self.dev_path, err)
1785

    
1786
  def Attach(self):
1787
    """Attach to an existing file.
1788

1789
    Check if this file already exists.
1790

1791
    @rtype: boolean
1792
    @return: True if file exists
1793

1794
    """
1795
    self.attached = os.path.exists(self.dev_path)
1796
    return self.attached
1797

    
1798
  def GetActualSize(self):
1799
    """Return the actual disk size.
1800

1801
    @note: the device needs to be active when this is called
1802

1803
    """
1804
    assert self.attached, "BlockDevice not attached in GetActualSize()"
1805
    try:
1806
      st = os.stat(self.dev_path)
1807
      return st.st_size
1808
    except OSError, err:
1809
      _ThrowError("Can't stat %s: %s", self.dev_path, err)
1810

    
1811
  @classmethod
1812
  def Create(cls, unique_id, children, size):
1813
    """Create a new file.
1814

1815
    @param size: the size of file in MiB
1816

1817
    @rtype: L{bdev.FileStorage}
1818
    @return: an instance of FileStorage
1819

1820
    """
1821
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1822
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1823
    dev_path = unique_id[1]
1824
    if os.path.exists(dev_path):
1825
      _ThrowError("File already existing: %s", dev_path)
1826
    try:
1827
      f = open(dev_path, 'w')
1828
      f.truncate(size * 1024 * 1024)
1829
      f.close()
1830
    except IOError, err:
1831
      _ThrowError("Error in file creation: %", str(err))
1832

    
1833
    return FileStorage(unique_id, children, size)
1834

    
1835

    
1836
DEV_MAP = {
1837
  constants.LD_LV: LogicalVolume,
1838
  constants.LD_DRBD8: DRBD8,
1839
  constants.LD_FILE: FileStorage,
1840
  }
1841

    
1842

    
1843
def FindDevice(dev_type, unique_id, children, size):
1844
  """Search for an existing, assembled device.
1845

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

1849
  """
1850
  if dev_type not in DEV_MAP:
1851
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1852
  device = DEV_MAP[dev_type](unique_id, children, size)
1853
  if not device.attached:
1854
    return None
1855
  return device
1856

    
1857

    
1858
def Assemble(dev_type, unique_id, children, size):
1859
  """Try to attach or assemble an existing device.
1860

1861
  This will attach to assemble the device, as needed, to bring it
1862
  fully up. It must be safe to run on already-assembled devices.
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
  device.Assemble()
1869
  return device
1870

    
1871

    
1872
def Create(dev_type, unique_id, children, size):
1873
  """Create a device.
1874

1875
  """
1876
  if dev_type not in DEV_MAP:
1877
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1878
  device = DEV_MAP[dev_type].Create(unique_id, children, size)
1879
  return device