Statistics
| Branch: | Tag: | Revision:

root / lib / bdev.py @ def8e2f6

History | View | Annotate | Download (60 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
    def _CheckNetworkConfig():
1229
      info = cls._GetDevInfo(cls._GetShowData(minor))
1230
      if not "local_addr" in info or not "remote_addr" in info:
1231
        raise utils.RetryAgain()
1232

    
1233
      if (info["local_addr"] != (lhost, lport) or
1234
          info["remote_addr"] != (rhost, rport)):
1235
        raise utils.RetryAgain()
1236

    
1237
    try:
1238
      utils.Retry(_CheckNetworkConfig, 1.0, 10.0)
1239
    except utils.RetryTimeout:
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
    class _DisconnectStatus:
1433
      def __init__(self, ever_disconnected):
1434
        self.ever_disconnected = ever_disconnected
1435

    
1436
    dstatus = _DisconnectStatus(_IgnoreError(self._ShutdownNet, self.minor))
1437

    
1438
    def _WaitForDisconnect():
1439
      if self.GetProcStatus().is_standalone:
1440
        return
1441

    
1442
      # retry the disconnect, it seems possible that due to a well-time
1443
      # disconnect on the peer, my disconnect command might be ignored and
1444
      # forgotten
1445
      dstatus.ever_disconnected = \
1446
        _IgnoreError(self._ShutdownNet, self.minor) or dstatus.ever_disconnected
1447

    
1448
      raise utils.RetryAgain()
1449

    
1450
    # Keep start time
1451
    start_time = time.time()
1452

    
1453
    try:
1454
      # Start delay at 100 milliseconds and grow up to 2 seconds
1455
      utils.Retry(_WaitForDisconnect, (0.1, 1.5, 2.0),
1456
                  self._NET_RECONFIG_TIMEOUT)
1457
    except utils.RetryTimeout:
1458
      if dstatus.ever_disconnected:
1459
        msg = ("drbd%d: device did not react to the"
1460
               " 'disconnect' command in a timely manner")
1461
      else:
1462
        msg = "drbd%d: can't shutdown network, even after multiple retries"
1463

    
1464
      _ThrowError(msg, self.minor)
1465

    
1466
    reconfig_time = time.time() - start_time
1467
    if reconfig_time > (self._NET_RECONFIG_TIMEOUT * 0.25):
1468
      logging.info("drbd%d: DisconnectNet: detach took %.3f seconds",
1469
                   self.minor, reconfig_time)
1470

    
1471
  def AttachNet(self, multimaster):
1472
    """Reconnects the network.
1473

1474
    This method connects the network side of the device with a
1475
    specified multi-master flag. The device needs to be 'Standalone'
1476
    but have valid network configuration data.
1477

1478
    Args:
1479
      - multimaster: init the network in dual-primary mode
1480

1481
    """
1482
    if self.minor is None:
1483
      _ThrowError("drbd%d: device not attached in AttachNet", self._aminor)
1484

    
1485
    if None in (self._lhost, self._lport, self._rhost, self._rport):
1486
      _ThrowError("drbd%d: missing network info in AttachNet()", self.minor)
1487

    
1488
    status = self.GetProcStatus()
1489

    
1490
    if not status.is_standalone:
1491
      _ThrowError("drbd%d: device is not standalone in AttachNet", self.minor)
1492

    
1493
    self._AssembleNet(self.minor,
1494
                      (self._lhost, self._lport, self._rhost, self._rport),
1495
                      constants.DRBD_NET_PROTOCOL, dual_pri=multimaster,
1496
                      hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1497

    
1498
  def Attach(self):
1499
    """Check if our minor is configured.
1500

1501
    This doesn't do any device configurations - it only checks if the
1502
    minor is in a state different from Unconfigured.
1503

1504
    Note that this function will not change the state of the system in
1505
    any way (except in case of side-effects caused by reading from
1506
    /proc).
1507

1508
    """
1509
    used_devs = self.GetUsedDevs()
1510
    if self._aminor in used_devs:
1511
      minor = self._aminor
1512
    else:
1513
      minor = None
1514

    
1515
    self._SetFromMinor(minor)
1516
    return minor is not None
1517

    
1518
  def Assemble(self):
1519
    """Assemble the drbd.
1520

1521
    Method:
1522
      - if we have a configured device, we try to ensure that it matches
1523
        our config
1524
      - if not, we create it from zero
1525

1526
    """
1527
    super(DRBD8, self).Assemble()
1528

    
1529
    self.Attach()
1530
    if self.minor is None:
1531
      # local device completely unconfigured
1532
      self._FastAssemble()
1533
    else:
1534
      # we have to recheck the local and network status and try to fix
1535
      # the device
1536
      self._SlowAssemble()
1537

    
1538
  def _SlowAssemble(self):
1539
    """Assembles the DRBD device from a (partially) configured device.
1540

1541
    In case of partially attached (local device matches but no network
1542
    setup), we perform the network attach. If successful, we re-test
1543
    the attach if can return success.
1544

1545
    """
1546
    net_data = (self._lhost, self._lport, self._rhost, self._rport)
1547
    for minor in (self._aminor,):
1548
      info = self._GetDevInfo(self._GetShowData(minor))
1549
      match_l = self._MatchesLocal(info)
1550
      match_r = self._MatchesNet(info)
1551

    
1552
      if match_l and match_r:
1553
        # everything matches
1554
        break
1555

    
1556
      if match_l and not match_r and "local_addr" not in info:
1557
        # disk matches, but not attached to network, attach and recheck
1558
        self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
1559
                          hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1560
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1561
          break
1562
        else:
1563
          _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
1564
                      " show' disagrees", minor)
1565

    
1566
      if match_r and "local_dev" not in info:
1567
        # no local disk, but network attached and it matches
1568
        self._AssembleLocal(minor, self._children[0].dev_path,
1569
                            self._children[1].dev_path, self.size)
1570
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1571
          break
1572
        else:
1573
          _ThrowError("drbd%d: disk attach successful, but 'drbdsetup"
1574
                      " show' disagrees", minor)
1575

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

    
1603
    else:
1604
      minor = None
1605

    
1606
    self._SetFromMinor(minor)
1607
    if minor is None:
1608
      _ThrowError("drbd%d: cannot activate, unknown or unhandled reason",
1609
                  self._aminor)
1610

    
1611
  def _FastAssemble(self):
1612
    """Assemble the drbd device from zero.
1613

1614
    This is run when in Assemble we detect our minor is unused.
1615

1616
    """
1617
    minor = self._aminor
1618
    if self._children and self._children[0] and self._children[1]:
1619
      self._AssembleLocal(minor, self._children[0].dev_path,
1620
                          self._children[1].dev_path, self.size)
1621
    if self._lhost and self._lport and self._rhost and self._rport:
1622
      self._AssembleNet(minor,
1623
                        (self._lhost, self._lport, self._rhost, self._rport),
1624
                        constants.DRBD_NET_PROTOCOL,
1625
                        hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1626
    self._SetFromMinor(minor)
1627

    
1628
  @classmethod
1629
  def _ShutdownLocal(cls, minor):
1630
    """Detach from the local device.
1631

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

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

    
1640
  @classmethod
1641
  def _ShutdownNet(cls, minor):
1642
    """Disconnect from the remote peer.
1643

1644
    This fails if we don't have a local device.
1645

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

    
1651
  @classmethod
1652
  def _ShutdownAll(cls, minor):
1653
    """Deactivate the device.
1654

1655
    This will, of course, fail if the device is in use.
1656

1657
    """
1658
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "down"])
1659
    if result.failed:
1660
      _ThrowError("drbd%d: can't shutdown drbd device: %s",
1661
                  minor, result.output)
1662

    
1663
  def Shutdown(self):
1664
    """Shutdown the DRBD device.
1665

1666
    """
1667
    if self.minor is None and not self.Attach():
1668
      logging.info("drbd%d: not attached during Shutdown()", self._aminor)
1669
      return
1670
    minor = self.minor
1671
    self.minor = None
1672
    self.dev_path = None
1673
    self._ShutdownAll(minor)
1674

    
1675
  def Remove(self):
1676
    """Stub remove for DRBD devices.
1677

1678
    """
1679
    self.Shutdown()
1680

    
1681
  @classmethod
1682
  def Create(cls, unique_id, children, size):
1683
    """Create a new DRBD8 device.
1684

1685
    Since DRBD devices are not created per se, just assembled, this
1686
    function only initializes the metadata.
1687

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

    
1710
  def Grow(self, amount):
1711
    """Resize the DRBD device and its backing storage.
1712

1713
    """
1714
    if self.minor is None:
1715
      _ThrowError("drbd%d: Grow called while not attached", self._aminor)
1716
    if len(self._children) != 2 or None in self._children:
1717
      _ThrowError("drbd%d: cannot grow diskless device", self.minor)
1718
    self._children[0].Grow(amount)
1719
    result = utils.RunCmd(["drbdsetup", self.dev_path, "resize", "-s",
1720
                           "%dm" % (self.size + amount)])
1721
    if result.failed:
1722
      _ThrowError("drbd%d: resize failed: %s", self.minor, result.output)
1723

    
1724

    
1725
class FileStorage(BlockDev):
1726
  """File device.
1727

1728
  This class represents the a file storage backend device.
1729

1730
  The unique_id for the file device is a (file_driver, file_path) tuple.
1731

1732
  """
1733
  def __init__(self, unique_id, children, size):
1734
    """Initalizes a file device backend.
1735

1736
    """
1737
    if children:
1738
      raise errors.BlockDeviceError("Invalid setup for file device")
1739
    super(FileStorage, self).__init__(unique_id, children, size)
1740
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1741
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1742
    self.driver = unique_id[0]
1743
    self.dev_path = unique_id[1]
1744
    self.Attach()
1745

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

1749
    Checks whether the file device exists, raises BlockDeviceError otherwise.
1750

1751
    """
1752
    if not os.path.exists(self.dev_path):
1753
      _ThrowError("File device '%s' does not exist" % self.dev_path)
1754

    
1755
  def Shutdown(self):
1756
    """Shutdown the device.
1757

1758
    This is a no-op for the file type, as we don't deactivate
1759
    the file on shutdown.
1760

1761
    """
1762
    pass
1763

    
1764
  def Open(self, force=False):
1765
    """Make the device ready for I/O.
1766

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

1769
    """
1770
    pass
1771

    
1772
  def Close(self):
1773
    """Notifies that the device will no longer be used for I/O.
1774

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

1777
    """
1778
    pass
1779

    
1780
  def Remove(self):
1781
    """Remove the file backing the block device.
1782

1783
    @rtype: boolean
1784
    @return: True if the removal was successful
1785

1786
    """
1787
    try:
1788
      os.remove(self.dev_path)
1789
    except OSError, err:
1790
      if err.errno != errno.ENOENT:
1791
        _ThrowError("Can't remove file '%s': %s", self.dev_path, err)
1792

    
1793
  def Attach(self):
1794
    """Attach to an existing file.
1795

1796
    Check if this file already exists.
1797

1798
    @rtype: boolean
1799
    @return: True if file exists
1800

1801
    """
1802
    self.attached = os.path.exists(self.dev_path)
1803
    return self.attached
1804

    
1805
  def GetActualSize(self):
1806
    """Return the actual disk size.
1807

1808
    @note: the device needs to be active when this is called
1809

1810
    """
1811
    assert self.attached, "BlockDevice not attached in GetActualSize()"
1812
    try:
1813
      st = os.stat(self.dev_path)
1814
      return st.st_size
1815
    except OSError, err:
1816
      _ThrowError("Can't stat %s: %s", self.dev_path, err)
1817

    
1818
  @classmethod
1819
  def Create(cls, unique_id, children, size):
1820
    """Create a new file.
1821

1822
    @param size: the size of file in MiB
1823

1824
    @rtype: L{bdev.FileStorage}
1825
    @return: an instance of FileStorage
1826

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

    
1840
    return FileStorage(unique_id, children, size)
1841

    
1842

    
1843
DEV_MAP = {
1844
  constants.LD_LV: LogicalVolume,
1845
  constants.LD_DRBD8: DRBD8,
1846
  constants.LD_FILE: FileStorage,
1847
  }
1848

    
1849

    
1850
def FindDevice(dev_type, unique_id, children, size):
1851
  """Search for an existing, assembled device.
1852

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

1856
  """
1857
  if dev_type not in DEV_MAP:
1858
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1859
  device = DEV_MAP[dev_type](unique_id, children, size)
1860
  if not device.attached:
1861
    return None
1862
  return device
1863

    
1864

    
1865
def Assemble(dev_type, unique_id, children, size):
1866
  """Try to attach or assemble an existing device.
1867

1868
  This will attach to assemble the device, as needed, to bring it
1869
  fully up. It must be safe to run on already-assembled devices.
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](unique_id, children, size)
1875
  device.Assemble()
1876
  return device
1877

    
1878

    
1879
def Create(dev_type, unique_id, children, size):
1880
  """Create a device.
1881

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