Statistics
| Branch: | Tag: | Revision:

root / lib / bdev.py @ 0757c107

History | View | Annotate | Download (59.9 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
      data = utils.ReadFile(filename).splitlines()
784
    except EnvironmentError, err:
785
      if err.errno == errno.ENOENT:
786
        _ThrowError("The file %s cannot be opened, check if the module"
787
                    " is loaded (%s)", filename, str(err))
788
      else:
789
        _ThrowError("Can't read the DRBD proc file %s: %s", filename, str(err))
790
    if not data:
791
      _ThrowError("Can't read any data from %s", filename)
792
    return data
793

    
794
  @staticmethod
795
  def _MassageProcData(data):
796
    """Transform the output of _GetProdData into a nicer form.
797

798
    @return: a dictionary of minor: joined lines from /proc/drbd
799
        for that minor
800

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

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

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

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

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

    
852
    return retval
853

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

858
    """
859
    return "/dev/drbd%d" % minor
860

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

865
    """
866
    data = cls._GetProcData()
867

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

    
880
    return used_devs
881

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

885
    This sets our minor variable and our dev_path.
886

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

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

900
    This currently only check the size, which must be around
901
    128MiB.
902

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

    
924
  def Rename(self, new_id):
925
    """Rename a device.
926

927
    This is not supported for drbd devices.
928

929
    """
930
    raise errors.ProgrammerError("Can't rename a drbd device")
931

    
932

    
933
class DRBD8(BaseDRBD):
934
  """DRBD v8.x block device.
935

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

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

945
  """
946
  _MAX_MINORS = 255
947
  _PARSE_SHOW = None
948

    
949
  # timeout constants
950
  _NET_RECONFIG_TIMEOUT = 60
951

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

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

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

980
    This will not work if the given minor is in use.
981

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

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

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

995
    """
996
    data = cls._GetProcData()
997

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

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

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

1023
    """
1024
    if cls._PARSE_SHOW is not None:
1025
      return cls._PARSE_SHOW
1026

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

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

    
1038
    keyword = pyp.Word(pyp.alphanums + '-')
1039

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

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

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

    
1064
    bnf = pyp.ZeroOrMore(pyp.Group(section ^ stmt))
1065
    bnf.ignore(comment)
1066

    
1067
    cls._PARSE_SHOW = bnf
1068

    
1069
    return bnf
1070

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

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

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

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

1092
    """
1093
    data = {}
1094
    if not out:
1095
      return data
1096

    
1097
    bnf = cls._GetShowParser()
1098
    # run pyparse
1099

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

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

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

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

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

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

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

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

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

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

    
1165
    if self._lhost is None:
1166
      return False
1167

    
1168
    if not ("local_addr" in info and
1169
            "remote_addr" in info):
1170
      return False
1171

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

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

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

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

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

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

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

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

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

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

    
1264
    self._AssembleLocal(self.minor, backend.dev_path, meta.dev_path, self.size)
1265
    self._children = devices
1266

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

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

    
1291
    self._ShutdownLocal(self.minor)
1292
    self._children = []
1293

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

1298
    This is the low-level implementation.
1299

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

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

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

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

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

    
1330
  def GetProcStatus(self):
1331
    """Return device data from /proc.
1332

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

    
1341
  def GetSyncStatus(self):
1342
    """Returns the sync status of the device.
1343

1344

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

1349

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

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

1356
    @rtype: objects.BlockDevStatus
1357

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

    
1362
    stats = self.GetProcStatus()
1363
    is_degraded = not stats.is_connected or not stats.is_disk_uptodate
1364

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

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

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

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

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

    
1400
  def Close(self):
1401
    """Make the local state secondary.
1402

1403
    This will, of course, fail if the device is in use.
1404

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

    
1413
  def DisconnectNet(self):
1414
    """Removes network configuration.
1415

1416
    This method shutdowns the network side of the device.
1417

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

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

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

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

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

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

    
1462
  def AttachNet(self, multimaster):
1463
    """Reconnects the network.
1464

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

1469
    Args:
1470
      - multimaster: init the network in dual-primary mode
1471

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

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

    
1479
    status = self.GetProcStatus()
1480

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

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

    
1489
  def Attach(self):
1490
    """Check if our minor is configured.
1491

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

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

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

    
1506
    self._SetFromMinor(minor)
1507
    return minor is not None
1508

    
1509
  def Assemble(self):
1510
    """Assemble the drbd.
1511

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

1517
    """
1518
    super(DRBD8, self).Assemble()
1519

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

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

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

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

    
1543
      if match_l and match_r:
1544
        # everything matches
1545
        break
1546

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

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

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

    
1594
    else:
1595
      minor = None
1596

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

    
1602
  def _FastAssemble(self):
1603
    """Assemble the drbd device from zero.
1604

1605
    This is run when in Assemble we detect our minor is unused.
1606

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

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

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

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

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

1635
    This fails if we don't have a local device.
1636

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

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

1646
    This will, of course, fail if the device is in use.
1647

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

    
1654
  def Shutdown(self):
1655
    """Shutdown the DRBD device.
1656

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

    
1666
  def Remove(self):
1667
    """Stub remove for DRBD devices.
1668

1669
    """
1670
    self.Shutdown()
1671

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

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

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

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

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

    
1715

    
1716
class FileStorage(BlockDev):
1717
  """File device.
1718

1719
  This class represents the a file storage backend device.
1720

1721
  The unique_id for the file device is a (file_driver, file_path) tuple.
1722

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

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

    
1737
  def Assemble(self):
1738
    """Assemble the device.
1739

1740
    Checks whether the file device exists, raises BlockDeviceError otherwise.
1741

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

    
1746
  def Shutdown(self):
1747
    """Shutdown the device.
1748

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

1752
    """
1753
    pass
1754

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

1758
    This is a no-op for the file type.
1759

1760
    """
1761
    pass
1762

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

1766
    This is a no-op for the file type.
1767

1768
    """
1769
    pass
1770

    
1771
  def Remove(self):
1772
    """Remove the file backing the block device.
1773

1774
    @rtype: boolean
1775
    @return: True if the removal was successful
1776

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

    
1784
  def Attach(self):
1785
    """Attach to an existing file.
1786

1787
    Check if this file already exists.
1788

1789
    @rtype: boolean
1790
    @return: True if file exists
1791

1792
    """
1793
    self.attached = os.path.exists(self.dev_path)
1794
    return self.attached
1795

    
1796
  def GetActualSize(self):
1797
    """Return the actual disk size.
1798

1799
    @note: the device needs to be active when this is called
1800

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

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

1813
    @param size: the size of file in MiB
1814

1815
    @rtype: L{bdev.FileStorage}
1816
    @return: an instance of FileStorage
1817

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

    
1831
    return FileStorage(unique_id, children, size)
1832

    
1833

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

    
1840

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

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

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

    
1855

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

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

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

    
1869

    
1870
def Create(dev_type, unique_id, children, size):
1871
  """Create a device.
1872

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