Statistics
| Branch: | Tag: | Revision:

root / lib / bdev.py @ 19bed813

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
      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
      lresult = lmatch.match(line)
807
      if lresult is not None:
808
        if old_minor is not None:
809
          results[old_minor] = old_line
810
        old_minor = int(lresult.group(1))
811
        old_line = line
812
      else:
813
        if old_minor is not None:
814
          old_line += " " + line.strip()
815
    # add last line
816
    if old_minor is not None:
817
      results[old_minor] = old_line
818
    return results
819

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

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

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

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

    
850
    return retval
851

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

856
    """
857
    return "/dev/drbd%d" % minor
858

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

863
    """
864
    data = cls._GetProcData()
865

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

    
878
    return used_devs
879

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

883
    This sets our minor variable and our dev_path.
884

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

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

898
    This currently only check the size, which must be around
899
    128MiB.
900

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

    
922
  def Rename(self, new_id):
923
    """Rename a device.
924

925
    This is not supported for drbd devices.
926

927
    """
928
    raise errors.ProgrammerError("Can't rename a drbd device")
929

    
930

    
931
class DRBD8(BaseDRBD):
932
  """DRBD v8.x block device.
933

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

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

943
  """
944
  _MAX_MINORS = 255
945
  _PARSE_SHOW = None
946

    
947
  # timeout constants
948
  _NET_RECONFIG_TIMEOUT = 60
949

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

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

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

978
    This will not work if the given minor is in use.
979

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

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

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

993
    """
994
    data = cls._GetProcData()
995

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

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

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

1021
    """
1022
    if cls._PARSE_SHOW is not None:
1023
      return cls._PARSE_SHOW
1024

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

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

    
1036
    keyword = pyp.Word(pyp.alphanums + '-')
1037

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

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

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

    
1062
    bnf = pyp.ZeroOrMore(pyp.Group(section ^ stmt))
1063
    bnf.ignore(comment)
1064

    
1065
    cls._PARSE_SHOW = bnf
1066

    
1067
    return bnf
1068

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

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

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

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

1090
    """
1091
    data = {}
1092
    if not out:
1093
      return data
1094

    
1095
    bnf = cls._GetShowParser()
1096
    # run pyparse
1097

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

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

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

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

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

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

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

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

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

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

    
1163
    if self._lhost is None:
1164
      return False
1165

    
1166
    if not ("local_addr" in info and
1167
            "remote_addr" in info):
1168
      return False
1169

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

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

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

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

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

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

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

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

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

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

    
1262
    self._AssembleLocal(self.minor, backend.dev_path, meta.dev_path, self.size)
1263
    self._children = devices
1264

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

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

    
1289
    self._ShutdownLocal(self.minor)
1290
    self._children = []
1291

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

1296
    This is the low-level implementation.
1297

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

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

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

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

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

    
1328
  def GetProcStatus(self):
1329
    """Return device data from /proc.
1330

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

    
1339
  def GetSyncStatus(self):
1340
    """Returns the sync status of the device.
1341

1342

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

1347

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

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

1354
    @rtype: objects.BlockDevStatus
1355

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

    
1360
    stats = self.GetProcStatus()
1361
    is_degraded = not stats.is_connected or not stats.is_disk_uptodate
1362

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

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

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

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

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

    
1398
  def Close(self):
1399
    """Make the local state secondary.
1400

1401
    This will, of course, fail if the device is in use.
1402

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

    
1411
  def DisconnectNet(self):
1412
    """Removes network configuration.
1413

1414
    This method shutdowns the network side of the device.
1415

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

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

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

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

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

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

    
1460
  def AttachNet(self, multimaster):
1461
    """Reconnects the network.
1462

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

1467
    Args:
1468
      - multimaster: init the network in dual-primary mode
1469

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

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

    
1477
    status = self.GetProcStatus()
1478

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

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

    
1487
  def Attach(self):
1488
    """Check if our minor is configured.
1489

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

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

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

    
1504
    self._SetFromMinor(minor)
1505
    return minor is not None
1506

    
1507
  def Assemble(self):
1508
    """Assemble the drbd.
1509

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

1515
    """
1516
    super(DRBD8, self).Assemble()
1517

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

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

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

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

    
1541
      if match_l and match_r:
1542
        # everything matches
1543
        break
1544

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

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

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

    
1592
    else:
1593
      minor = None
1594

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

    
1600
  def _FastAssemble(self):
1601
    """Assemble the drbd device from zero.
1602

1603
    This is run when in Assemble we detect our minor is unused.
1604

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

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

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

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

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

1633
    This fails if we don't have a local device.
1634

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

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

1644
    This will, of course, fail if the device is in use.
1645

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

    
1652
  def Shutdown(self):
1653
    """Shutdown the DRBD device.
1654

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

    
1664
  def Remove(self):
1665
    """Stub remove for DRBD devices.
1666

1667
    """
1668
    self.Shutdown()
1669

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

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

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

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

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

    
1713

    
1714
class FileStorage(BlockDev):
1715
  """File device.
1716

1717
  This class represents the a file storage backend device.
1718

1719
  The unique_id for the file device is a (file_driver, file_path) tuple.
1720

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

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

    
1735
  def Assemble(self):
1736
    """Assemble the device.
1737

1738
    Checks whether the file device exists, raises BlockDeviceError otherwise.
1739

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

    
1744
  def Shutdown(self):
1745
    """Shutdown the device.
1746

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

1750
    """
1751
    pass
1752

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

1756
    This is a no-op for the file type.
1757

1758
    """
1759
    pass
1760

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

1764
    This is a no-op for the file type.
1765

1766
    """
1767
    pass
1768

    
1769
  def Remove(self):
1770
    """Remove the file backing the block device.
1771

1772
    @rtype: boolean
1773
    @return: True if the removal was successful
1774

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

    
1782
  def Attach(self):
1783
    """Attach to an existing file.
1784

1785
    Check if this file already exists.
1786

1787
    @rtype: boolean
1788
    @return: True if file exists
1789

1790
    """
1791
    self.attached = os.path.exists(self.dev_path)
1792
    return self.attached
1793

    
1794
  def GetActualSize(self):
1795
    """Return the actual disk size.
1796

1797
    @note: the device needs to be active when this is called
1798

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

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

1811
    @param size: the size of file in MiB
1812

1813
    @rtype: L{bdev.FileStorage}
1814
    @return: an instance of FileStorage
1815

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

    
1829
    return FileStorage(unique_id, children, size)
1830

    
1831

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

    
1838

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

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

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

    
1853

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

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

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

    
1867

    
1868
def Create(dev_type, unique_id, children, size):
1869
  """Create a device.
1870

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