Statistics
| Branch: | Tag: | Revision:

root / lib / bdev.py @ 691744c4

History | View | Annotate | Download (58 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

    
35

    
36
def _IgnoreError(fn, *args, **kwargs):
37
  """Executes the given function, ignoring BlockDeviceErrors.
38

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

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

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

    
53

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

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

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

    
67

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

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

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

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

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

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

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

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

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

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

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

131
    """
132
    pass
133

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

137
    """
138
    raise NotImplementedError
139

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

143
    """
144
    raise NotImplementedError
145

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

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

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

157
    """
158
    raise NotImplementedError
159

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

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

167
    """
168
    raise NotImplementedError
169

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

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

175
    """
176
    raise NotImplementedError
177

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

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

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

187
    """
188
    raise NotImplementedError
189

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

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

197
    """
198
    raise NotImplementedError
199

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

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

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

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

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

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

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

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

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

231
    @rtype: tuple
232
    @return: (sync_percent, estimated_time, is_degraded, ldisk)
233

234
    """
235
    return None, None, False, False
236

    
237

    
238
  def CombinedSyncStatus(self):
239
    """Calculate the mirror status recursively for our children.
240

241
    The return value is the same as for `GetSyncStatus()` except the
242
    minimum percent and maximum time are calculated across our
243
    children.
244

245
    """
246
    min_percent, max_time, is_degraded, ldisk = self.GetSyncStatus()
247
    if self._children:
248
      for child in self._children:
249
        c_percent, c_time, c_degraded, c_ldisk = child.GetSyncStatus()
250
        if min_percent is None:
251
          min_percent = c_percent
252
        elif c_percent is not None:
253
          min_percent = min(min_percent, c_percent)
254
        if max_time is None:
255
          max_time = c_time
256
        elif c_time is not None:
257
          max_time = max(max_time, c_time)
258
        is_degraded = is_degraded or c_degraded
259
        ldisk = ldisk or c_ldisk
260
    return min_percent, max_time, is_degraded, ldisk
261

    
262

    
263
  def SetInfo(self, text):
264
    """Update metadata with info text.
265

266
    Only supported for some device types.
267

268
    """
269
    for child in self._children:
270
      child.SetInfo(text)
271

    
272
  def Grow(self, amount):
273
    """Grow the block device.
274

275
    @param amount: the amount (in mebibytes) to grow with
276

277
    """
278
    raise NotImplementedError
279

    
280
  def GetActualSize(self):
281
    """Return the actual disk size.
282

283
    @note: the device needs to be active when this is called
284

285
    """
286
    assert self.attached, "BlockDevice not attached in GetActualSize()"
287
    result = utils.RunCmd(["blockdev", "--getsize64", self.dev_path])
288
    if result.failed:
289
      _ThrowError("blockdev failed (%s): %s",
290
                  result.fail_reason, result.output)
291
    try:
292
      sz = int(result.output.strip())
293
    except (ValueError, TypeError), err:
294
      _ThrowError("Failed to parse blockdev output: %s", str(err))
295
    return sz
296

    
297
  def __repr__(self):
298
    return ("<%s: unique_id: %s, children: %s, %s:%s, %s>" %
299
            (self.__class__, self.unique_id, self._children,
300
             self.major, self.minor, self.dev_path))
301

    
302

    
303
class LogicalVolume(BlockDev):
304
  """Logical Volume block device.
305

306
  """
307
  def __init__(self, unique_id, children, size):
308
    """Attaches to a LV device.
309

310
    The unique_id is a tuple (vg_name, lv_name)
311

312
    """
313
    super(LogicalVolume, self).__init__(unique_id, children, size)
314
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
315
      raise ValueError("Invalid configuration data %s" % str(unique_id))
316
    self._vg_name, self._lv_name = unique_id
317
    self.dev_path = "/dev/%s/%s" % (self._vg_name, self._lv_name)
318
    self._degraded = True
319
    self.major = self.minor = self.pe_size = self.stripe_count = None
320
    self.Attach()
321

    
322
  @classmethod
323
  def Create(cls, unique_id, children, size):
324
    """Create a new logical volume.
325

326
    """
327
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
328
      raise errors.ProgrammerError("Invalid configuration data %s" %
329
                                   str(unique_id))
330
    vg_name, lv_name = unique_id
331
    pvs_info = cls.GetPVInfo(vg_name)
332
    if not pvs_info:
333
      _ThrowError("Can't compute PV info for vg %s", vg_name)
334
    pvs_info.sort()
335
    pvs_info.reverse()
336

    
337
    pvlist = [ pv[1] for pv in pvs_info ]
338
    free_size = sum([ pv[0] for pv in pvs_info ])
339
    current_pvs = len(pvlist)
340
    stripes = min(current_pvs, constants.LVM_STRIPECOUNT)
341

    
342
    # The size constraint should have been checked from the master before
343
    # calling the create function.
344
    if free_size < size:
345
      _ThrowError("Not enough free space: required %s,"
346
                  " available %s", size, free_size)
347
    cmd = ["lvcreate", "-L%dm" % size, "-n%s" % lv_name]
348
    # If the free space is not well distributed, we won't be able to
349
    # create an optimally-striped volume; in that case, we want to try
350
    # with N, N-1, ..., 2, and finally 1 (non-stripped) number of
351
    # stripes
352
    for stripes_arg in range(stripes, 0, -1):
353
      result = utils.RunCmd(cmd + ["-i%d" % stripes_arg] + [vg_name] + pvlist)
354
      if not result.failed:
355
        break
356
    if result.failed:
357
      _ThrowError("LV create failed (%s): %s",
358
                  result.fail_reason, result.output)
359
    return LogicalVolume(unique_id, children, size)
360

    
361
  @staticmethod
362
  def GetPVInfo(vg_name):
363
    """Get the free space info for PVs in a volume group.
364

365
    @param vg_name: the volume group name
366

367
    @rtype: list
368
    @return: list of tuples (free_space, name) with free_space in mebibytes
369

370
    """
371
    command = ["pvs", "--noheadings", "--nosuffix", "--units=m",
372
               "-opv_name,vg_name,pv_free,pv_attr", "--unbuffered",
373
               "--separator=:"]
374
    result = utils.RunCmd(command)
375
    if result.failed:
376
      logging.error("Can't get the PV information: %s - %s",
377
                    result.fail_reason, result.output)
378
      return None
379
    data = []
380
    for line in result.stdout.splitlines():
381
      fields = line.strip().split(':')
382
      if len(fields) != 4:
383
        logging.error("Can't parse pvs output: line '%s'", line)
384
        return None
385
      # skip over pvs from another vg or ones which are not allocatable
386
      if fields[1] != vg_name or fields[3][0] != 'a':
387
        continue
388
      data.append((float(fields[2]), fields[0]))
389

    
390
    return data
391

    
392
  def Remove(self):
393
    """Remove this logical volume.
394

395
    """
396
    if not self.minor and not self.Attach():
397
      # the LV does not exist
398
      return
399
    result = utils.RunCmd(["lvremove", "-f", "%s/%s" %
400
                           (self._vg_name, self._lv_name)])
401
    if result.failed:
402
      _ThrowError("Can't lvremove: %s - %s", result.fail_reason, result.output)
403

    
404
  def Rename(self, new_id):
405
    """Rename this logical volume.
406

407
    """
408
    if not isinstance(new_id, (tuple, list)) or len(new_id) != 2:
409
      raise errors.ProgrammerError("Invalid new logical id '%s'" % new_id)
410
    new_vg, new_name = new_id
411
    if new_vg != self._vg_name:
412
      raise errors.ProgrammerError("Can't move a logical volume across"
413
                                   " volume groups (from %s to to %s)" %
414
                                   (self._vg_name, new_vg))
415
    result = utils.RunCmd(["lvrename", new_vg, self._lv_name, new_name])
416
    if result.failed:
417
      _ThrowError("Failed to rename the logical volume: %s", result.output)
418
    self._lv_name = new_name
419
    self.dev_path = "/dev/%s/%s" % (self._vg_name, self._lv_name)
420

    
421
  def Attach(self):
422
    """Attach to an existing LV.
423

424
    This method will try to see if an existing and active LV exists
425
    which matches our name. If so, its major/minor will be
426
    recorded.
427

428
    """
429
    self.attached = False
430
    result = utils.RunCmd(["lvs", "--noheadings", "--separator=,",
431
                           "--units=m", "--nosuffix",
432
                           "-olv_attr,lv_kernel_major,lv_kernel_minor,"
433
                           "vg_extent_size,stripes", self.dev_path])
434
    if result.failed:
435
      logging.error("Can't find LV %s: %s, %s",
436
                    self.dev_path, result.fail_reason, result.output)
437
      return False
438
    # the output can (and will) have multiple lines for multi-segment
439
    # LVs, as the 'stripes' parameter is a segment one, so we take
440
    # only the last entry, which is the one we're interested in; note
441
    # that with LVM2 anyway the 'stripes' value must be constant
442
    # across segments, so this is a no-op actually
443
    out = result.stdout.splitlines()
444
    if not out: # totally empty result? splitlines() returns at least
445
                # one line for any non-empty string
446
      logging.error("Can't parse LVS output, no lines? Got '%s'", str(out))
447
      return False
448
    out = out[-1].strip().rstrip(',')
449
    out = out.split(",")
450
    if len(out) != 5:
451
      logging.error("Can't parse LVS output, len(%s) != 5", str(out))
452
      return False
453

    
454
    status, major, minor, pe_size, stripes = out
455
    if len(status) != 6:
456
      logging.error("lvs lv_attr is not 6 characters (%s)", status)
457
      return False
458

    
459
    try:
460
      major = int(major)
461
      minor = int(minor)
462
    except (TypeError, ValueError), err:
463
      logging.error("lvs major/minor cannot be parsed: %s", str(err))
464

    
465
    try:
466
      pe_size = int(float(pe_size))
467
    except (TypeError, ValueError), err:
468
      logging.error("Can't parse vg extent size: %s", err)
469
      return False
470

    
471
    try:
472
      stripes = int(stripes)
473
    except (TypeError, ValueError), err:
474
      logging.error("Can't parse the number of stripes: %s", err)
475
      return False
476

    
477
    self.major = major
478
    self.minor = minor
479
    self.pe_size = pe_size
480
    self.stripe_count = stripes
481
    self._degraded = status[0] == 'v' # virtual volume, i.e. doesn't backing
482
                                      # storage
483
    self.attached = True
484
    return True
485

    
486
  def Assemble(self):
487
    """Assemble the device.
488

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

493
    """
494
    result = utils.RunCmd(["lvchange", "-ay", self.dev_path])
495
    if result.failed:
496
      _ThrowError("Can't activate lv %s: %s", self.dev_path, result.output)
497

    
498
  def Shutdown(self):
499
    """Shutdown the device.
500

501
    This is a no-op for the LV device type, as we don't deactivate the
502
    volumes on shutdown.
503

504
    """
505
    pass
506

    
507
  def GetSyncStatus(self):
508
    """Returns the sync status of the device.
509

510
    If this device is a mirroring device, this function returns the
511
    status of the mirror.
512

513
    For logical volumes, sync_percent and estimated_time are always
514
    None (no recovery in progress, as we don't handle the mirrored LV
515
    case). The is_degraded parameter is the inverse of the ldisk
516
    parameter.
517

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

524
    The status was already read in Attach, so we just return it.
525

526
    @rtype: tuple
527
    @return: (sync_percent, estimated_time, is_degraded, ldisk)
528

529
    """
530
    return None, None, self._degraded, self._degraded
531

    
532
  def Open(self, force=False):
533
    """Make the device ready for I/O.
534

535
    This is a no-op for the LV device type.
536

537
    """
538
    pass
539

    
540
  def Close(self):
541
    """Notifies that the device will no longer be used for I/O.
542

543
    This is a no-op for the LV device type.
544

545
    """
546
    pass
547

    
548
  def Snapshot(self, size):
549
    """Create a snapshot copy of an lvm block device.
550

551
    """
552
    snap_name = self._lv_name + ".snap"
553

    
554
    # remove existing snapshot if found
555
    snap = LogicalVolume((self._vg_name, snap_name), None, size)
556
    _IgnoreError(snap.Remove)
557

    
558
    pvs_info = self.GetPVInfo(self._vg_name)
559
    if not pvs_info:
560
      _ThrowError("Can't compute PV info for vg %s", self._vg_name)
561
    pvs_info.sort()
562
    pvs_info.reverse()
563
    free_size, pv_name = pvs_info[0]
564
    if free_size < size:
565
      _ThrowError("Not enough free space: required %s,"
566
                  " available %s", size, free_size)
567

    
568
    result = utils.RunCmd(["lvcreate", "-L%dm" % size, "-s",
569
                           "-n%s" % snap_name, self.dev_path])
570
    if result.failed:
571
      _ThrowError("command: %s error: %s - %s",
572
                  result.cmd, result.fail_reason, result.output)
573

    
574
    return snap_name
575

    
576
  def SetInfo(self, text):
577
    """Update metadata with info text.
578

579
    """
580
    BlockDev.SetInfo(self, text)
581

    
582
    # Replace invalid characters
583
    text = re.sub('^[^A-Za-z0-9_+.]', '_', text)
584
    text = re.sub('[^-A-Za-z0-9_+.]', '_', text)
585

    
586
    # Only up to 128 characters are allowed
587
    text = text[:128]
588

    
589
    result = utils.RunCmd(["lvchange", "--addtag", text,
590
                           self.dev_path])
591
    if result.failed:
592
      _ThrowError("Command: %s error: %s - %s", result.cmd, result.fail_reason,
593
                  result.output)
594

    
595
  def Grow(self, amount):
596
    """Grow the logical volume.
597

598
    """
599
    if self.pe_size is None or self.stripe_count is None:
600
      if not self.Attach():
601
        _ThrowError("Can't attach to LV during Grow()")
602
    full_stripe_size = self.pe_size * self.stripe_count
603
    rest = amount % full_stripe_size
604
    if rest != 0:
605
      amount += full_stripe_size - rest
606
    # we try multiple algorithms since the 'best' ones might not have
607
    # space available in the right place, but later ones might (since
608
    # they have less constraints); also note that only recent LVM
609
    # supports 'cling'
610
    for alloc_policy in "contiguous", "cling", "normal":
611
      result = utils.RunCmd(["lvextend", "--alloc", alloc_policy,
612
                             "-L", "+%dm" % amount, self.dev_path])
613
      if not result.failed:
614
        return
615
    _ThrowError("Can't grow LV %s: %s", self.dev_path, result.output)
616

    
617

    
618
class DRBD8Status(object):
619
  """A DRBD status representation class.
620

621
  Note that this doesn't support unconfigured devices (cs:Unconfigured).
622

623
  """
624
  UNCONF_RE = re.compile(r"\s*[0-9]+:\s*cs:Unconfigured$")
625
  LINE_RE = re.compile(r"\s*[0-9]+:\s*cs:(\S+)\s+(?:st|ro):([^/]+)/(\S+)"
626
                       "\s+ds:([^/]+)/(\S+)\s+.*$")
627
  SYNC_RE = re.compile(r"^.*\ssync'ed:\s*([0-9.]+)%.*"
628
                       "\sfinish: ([0-9]+):([0-9]+):([0-9]+)\s.*$")
629

    
630
  CS_UNCONFIGURED = "Unconfigured"
631
  CS_STANDALONE = "StandAlone"
632
  CS_WFCONNECTION = "WFConnection"
633
  CS_WFREPORTPARAMS = "WFReportParams"
634
  CS_CONNECTED = "Connected"
635
  CS_STARTINGSYNCS = "StartingSyncS"
636
  CS_STARTINGSYNCT = "StartingSyncT"
637
  CS_WFBITMAPS = "WFBitMapS"
638
  CS_WFBITMAPT = "WFBitMapT"
639
  CS_WFSYNCUUID = "WFSyncUUID"
640
  CS_SYNCSOURCE = "SyncSource"
641
  CS_SYNCTARGET = "SyncTarget"
642
  CS_PAUSEDSYNCS = "PausedSyncS"
643
  CS_PAUSEDSYNCT = "PausedSyncT"
644
  CSET_SYNC = frozenset([
645
    CS_WFREPORTPARAMS,
646
    CS_STARTINGSYNCS,
647
    CS_STARTINGSYNCT,
648
    CS_WFBITMAPS,
649
    CS_WFBITMAPT,
650
    CS_WFSYNCUUID,
651
    CS_SYNCSOURCE,
652
    CS_SYNCTARGET,
653
    CS_PAUSEDSYNCS,
654
    CS_PAUSEDSYNCT,
655
    ])
656

    
657
  DS_DISKLESS = "Diskless"
658
  DS_ATTACHING = "Attaching" # transient state
659
  DS_FAILED = "Failed" # transient state, next: diskless
660
  DS_NEGOTIATING = "Negotiating" # transient state
661
  DS_INCONSISTENT = "Inconsistent" # while syncing or after creation
662
  DS_OUTDATED = "Outdated"
663
  DS_DUNKNOWN = "DUnknown" # shown for peer disk when not connected
664
  DS_CONSISTENT = "Consistent"
665
  DS_UPTODATE = "UpToDate" # normal state
666

    
667
  RO_PRIMARY = "Primary"
668
  RO_SECONDARY = "Secondary"
669
  RO_UNKNOWN = "Unknown"
670

    
671
  def __init__(self, procline):
672
    u = self.UNCONF_RE.match(procline)
673
    if u:
674
      self.cstatus = self.CS_UNCONFIGURED
675
      self.lrole = self.rrole = self.ldisk = self.rdisk = None
676
    else:
677
      m = self.LINE_RE.match(procline)
678
      if not m:
679
        raise errors.BlockDeviceError("Can't parse input data '%s'" % procline)
680
      self.cstatus = m.group(1)
681
      self.lrole = m.group(2)
682
      self.rrole = m.group(3)
683
      self.ldisk = m.group(4)
684
      self.rdisk = m.group(5)
685

    
686
    # end reading of data from the LINE_RE or UNCONF_RE
687

    
688
    self.is_standalone = self.cstatus == self.CS_STANDALONE
689
    self.is_wfconn = self.cstatus == self.CS_WFCONNECTION
690
    self.is_connected = self.cstatus == self.CS_CONNECTED
691
    self.is_primary = self.lrole == self.RO_PRIMARY
692
    self.is_secondary = self.lrole == self.RO_SECONDARY
693
    self.peer_primary = self.rrole == self.RO_PRIMARY
694
    self.peer_secondary = self.rrole == self.RO_SECONDARY
695
    self.both_primary = self.is_primary and self.peer_primary
696
    self.both_secondary = self.is_secondary and self.peer_secondary
697

    
698
    self.is_diskless = self.ldisk == self.DS_DISKLESS
699
    self.is_disk_uptodate = self.ldisk == self.DS_UPTODATE
700

    
701
    self.is_in_resync = self.cstatus in self.CSET_SYNC
702
    self.is_in_use = self.cstatus != self.CS_UNCONFIGURED
703

    
704
    m = self.SYNC_RE.match(procline)
705
    if m:
706
      self.sync_percent = float(m.group(1))
707
      hours = int(m.group(2))
708
      minutes = int(m.group(3))
709
      seconds = int(m.group(4))
710
      self.est_time = hours * 3600 + minutes * 60 + seconds
711
    else:
712
      # we have (in this if branch) no percent information, but if
713
      # we're resyncing we need to 'fake' a sync percent information,
714
      # as this is how cmdlib determines if it makes sense to wait for
715
      # resyncing or not
716
      if self.is_in_resync:
717
        self.sync_percent = 0
718
      else:
719
        self.sync_percent = None
720
      self.est_time = None
721

    
722

    
723
class BaseDRBD(BlockDev): # pylint: disable-msg=W0223
724
  """Base DRBD class.
725

726
  This class contains a few bits of common functionality between the
727
  0.7 and 8.x versions of DRBD.
728

729
  """
730
  _VERSION_RE = re.compile(r"^version: (\d+)\.(\d+)\.(\d+)"
731
                           r" \(api:(\d+)/proto:(\d+)(?:-(\d+))?\)")
732

    
733
  _DRBD_MAJOR = 147
734
  _ST_UNCONFIGURED = "Unconfigured"
735
  _ST_WFCONNECTION = "WFConnection"
736
  _ST_CONNECTED = "Connected"
737

    
738
  _STATUS_FILE = "/proc/drbd"
739

    
740
  @staticmethod
741
  def _GetProcData(filename=_STATUS_FILE):
742
    """Return data from /proc/drbd.
743

744
    """
745
    try:
746
      stat = open(filename, "r")
747
      try:
748
        data = stat.read().splitlines()
749
      finally:
750
        stat.close()
751
    except EnvironmentError, err:
752
      if err.errno == errno.ENOENT:
753
        _ThrowError("The file %s cannot be opened, check if the module"
754
                    " is loaded (%s)", filename, str(err))
755
      else:
756
        _ThrowError("Can't read the DRBD proc file %s: %s", filename, str(err))
757
    if not data:
758
      _ThrowError("Can't read any data from %s", filename)
759
    return data
760

    
761
  @staticmethod
762
  def _MassageProcData(data):
763
    """Transform the output of _GetProdData into a nicer form.
764

765
    @return: a dictionary of minor: joined lines from /proc/drbd
766
        for that minor
767

768
    """
769
    lmatch = re.compile("^ *([0-9]+):.*$")
770
    results = {}
771
    old_minor = old_line = None
772
    for line in data:
773
      if not line: # completely empty lines, as can be returned by drbd8.0+
774
        continue
775
      lresult = lmatch.match(line)
776
      if lresult is not None:
777
        if old_minor is not None:
778
          results[old_minor] = old_line
779
        old_minor = int(lresult.group(1))
780
        old_line = line
781
      else:
782
        if old_minor is not None:
783
          old_line += " " + line.strip()
784
    # add last line
785
    if old_minor is not None:
786
      results[old_minor] = old_line
787
    return results
788

    
789
  @classmethod
790
  def _GetVersion(cls):
791
    """Return the DRBD version.
792

793
    This will return a dict with keys:
794
      - k_major
795
      - k_minor
796
      - k_point
797
      - api
798
      - proto
799
      - proto2 (only on drbd > 8.2.X)
800

801
    """
802
    proc_data = cls._GetProcData()
803
    first_line = proc_data[0].strip()
804
    version = cls._VERSION_RE.match(first_line)
805
    if not version:
806
      raise errors.BlockDeviceError("Can't parse DRBD version from '%s'" %
807
                                    first_line)
808

    
809
    values = version.groups()
810
    retval = {'k_major': int(values[0]),
811
              'k_minor': int(values[1]),
812
              'k_point': int(values[2]),
813
              'api': int(values[3]),
814
              'proto': int(values[4]),
815
             }
816
    if values[5] is not None:
817
      retval['proto2'] = values[5]
818

    
819
    return retval
820

    
821
  @staticmethod
822
  def _DevPath(minor):
823
    """Return the path to a drbd device for a given minor.
824

825
    """
826
    return "/dev/drbd%d" % minor
827

    
828
  @classmethod
829
  def GetUsedDevs(cls):
830
    """Compute the list of used DRBD devices.
831

832
    """
833
    data = cls._GetProcData()
834

    
835
    used_devs = {}
836
    valid_line = re.compile("^ *([0-9]+): cs:([^ ]+).*$")
837
    for line in data:
838
      match = valid_line.match(line)
839
      if not match:
840
        continue
841
      minor = int(match.group(1))
842
      state = match.group(2)
843
      if state == cls._ST_UNCONFIGURED:
844
        continue
845
      used_devs[minor] = state, line
846

    
847
    return used_devs
848

    
849
  def _SetFromMinor(self, minor):
850
    """Set our parameters based on the given minor.
851

852
    This sets our minor variable and our dev_path.
853

854
    """
855
    if minor is None:
856
      self.minor = self.dev_path = None
857
      self.attached = False
858
    else:
859
      self.minor = minor
860
      self.dev_path = self._DevPath(minor)
861
      self.attached = True
862

    
863
  @staticmethod
864
  def _CheckMetaSize(meta_device):
865
    """Check if the given meta device looks like a valid one.
866

867
    This currently only check the size, which must be around
868
    128MiB.
869

870
    """
871
    result = utils.RunCmd(["blockdev", "--getsize", meta_device])
872
    if result.failed:
873
      _ThrowError("Failed to get device size: %s - %s",
874
                  result.fail_reason, result.output)
875
    try:
876
      sectors = int(result.stdout)
877
    except (TypeError, ValueError):
878
      _ThrowError("Invalid output from blockdev: '%s'", result.stdout)
879
    bytes = sectors * 512
880
    if bytes < 128 * 1024 * 1024: # less than 128MiB
881
      _ThrowError("Meta device too small (%.2fMib)", (bytes / 1024 / 1024))
882
    # the maximum *valid* size of the meta device when living on top
883
    # of LVM is hard to compute: it depends on the number of stripes
884
    # and the PE size; e.g. a 2-stripe, 64MB PE will result in a 128MB
885
    # (normal size), but an eight-stripe 128MB PE will result in a 1GB
886
    # size meta device; as such, we restrict it to 1GB (a little bit
887
    # too generous, but making assumptions about PE size is hard)
888
    if bytes > 1024 * 1024 * 1024:
889
      _ThrowError("Meta device too big (%.2fMiB)", (bytes / 1024 / 1024))
890

    
891
  def Rename(self, new_id):
892
    """Rename a device.
893

894
    This is not supported for drbd devices.
895

896
    """
897
    raise errors.ProgrammerError("Can't rename a drbd device")
898

    
899

    
900
class DRBD8(BaseDRBD):
901
  """DRBD v8.x block device.
902

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

907
  The unique_id for the drbd device is the (local_ip, local_port,
908
  remote_ip, remote_port) tuple, and it must have two children: the
909
  data device and the meta_device. The meta device is checked for
910
  valid size and is zeroed on create.
911

912
  """
913
  _MAX_MINORS = 255
914
  _PARSE_SHOW = None
915

    
916
  # timeout constants
917
  _NET_RECONFIG_TIMEOUT = 60
918

    
919
  def __init__(self, unique_id, children, size):
920
    if children and children.count(None) > 0:
921
      children = []
922
    super(DRBD8, self).__init__(unique_id, children, size)
923
    self.major = self._DRBD_MAJOR
924
    version = self._GetVersion()
925
    if version['k_major'] != 8 :
926
      _ThrowError("Mismatch in DRBD kernel version and requested ganeti"
927
                  " usage: kernel is %s.%s, ganeti wants 8.x",
928
                  version['k_major'], version['k_minor'])
929

    
930
    if len(children) not in (0, 2):
931
      raise ValueError("Invalid configuration data %s" % str(children))
932
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 6:
933
      raise ValueError("Invalid configuration data %s" % str(unique_id))
934
    (self._lhost, self._lport,
935
     self._rhost, self._rport,
936
     self._aminor, self._secret) = unique_id
937
    if (self._lhost is not None and self._lhost == self._rhost and
938
        self._lport == self._rport):
939
      raise ValueError("Invalid configuration data, same local/remote %s" %
940
                       (unique_id,))
941
    self.Attach()
942

    
943
  @classmethod
944
  def _InitMeta(cls, minor, dev_path):
945
    """Initialize a meta device.
946

947
    This will not work if the given minor is in use.
948

949
    """
950
    result = utils.RunCmd(["drbdmeta", "--force", cls._DevPath(minor),
951
                           "v08", dev_path, "0", "create-md"])
952
    if result.failed:
953
      _ThrowError("Can't initialize meta device: %s", result.output)
954

    
955
  @classmethod
956
  def _FindUnusedMinor(cls):
957
    """Find an unused DRBD device.
958

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

962
    """
963
    data = cls._GetProcData()
964

    
965
    unused_line = re.compile("^ *([0-9]+): cs:Unconfigured$")
966
    used_line = re.compile("^ *([0-9]+): cs:")
967
    highest = None
968
    for line in data:
969
      match = unused_line.match(line)
970
      if match:
971
        return int(match.group(1))
972
      match = used_line.match(line)
973
      if match:
974
        minor = int(match.group(1))
975
        highest = max(highest, minor)
976
    if highest is None: # there are no minors in use at all
977
      return 0
978
    if highest >= cls._MAX_MINORS:
979
      logging.error("Error: no free drbd minors!")
980
      raise errors.BlockDeviceError("Can't find a free DRBD minor")
981
    return highest + 1
982

    
983
  @classmethod
984
  def _GetShowParser(cls):
985
    """Return a parser for `drbd show` output.
986

987
    This will either create or return an already-create parser for the
988
    output of the command `drbd show`.
989

990
    """
991
    if cls._PARSE_SHOW is not None:
992
      return cls._PARSE_SHOW
993

    
994
    # pyparsing setup
995
    lbrace = pyp.Literal("{").suppress()
996
    rbrace = pyp.Literal("}").suppress()
997
    semi = pyp.Literal(";").suppress()
998
    # this also converts the value to an int
999
    number = pyp.Word(pyp.nums).setParseAction(lambda s, l, t: int(t[0]))
1000

    
1001
    comment = pyp.Literal ("#") + pyp.Optional(pyp.restOfLine)
1002
    defa = pyp.Literal("_is_default").suppress()
1003
    dbl_quote = pyp.Literal('"').suppress()
1004

    
1005
    keyword = pyp.Word(pyp.alphanums + '-')
1006

    
1007
    # value types
1008
    value = pyp.Word(pyp.alphanums + '_-/.:')
1009
    quoted = dbl_quote + pyp.CharsNotIn('"') + dbl_quote
1010
    addr_type = (pyp.Optional(pyp.Literal("ipv4")).suppress() +
1011
                 pyp.Optional(pyp.Literal("ipv6")).suppress())
1012
    addr_port = (addr_type + pyp.Word(pyp.nums + '.') +
1013
                 pyp.Literal(':').suppress() + number)
1014
    # meta device, extended syntax
1015
    meta_value = ((value ^ quoted) + pyp.Literal('[').suppress() +
1016
                  number + pyp.Word(']').suppress())
1017
    # device name, extended syntax
1018
    device_value = pyp.Literal("minor").suppress() + number
1019

    
1020
    # a statement
1021
    stmt = (~rbrace + keyword + ~lbrace +
1022
            pyp.Optional(addr_port ^ value ^ quoted ^ meta_value ^
1023
                         device_value) +
1024
            pyp.Optional(defa) + semi +
1025
            pyp.Optional(pyp.restOfLine).suppress())
1026

    
1027
    # an entire section
1028
    section_name = pyp.Word(pyp.alphas + '_')
1029
    section = section_name + lbrace + pyp.ZeroOrMore(pyp.Group(stmt)) + rbrace
1030

    
1031
    bnf = pyp.ZeroOrMore(pyp.Group(section ^ stmt))
1032
    bnf.ignore(comment)
1033

    
1034
    cls._PARSE_SHOW = bnf
1035

    
1036
    return bnf
1037

    
1038
  @classmethod
1039
  def _GetShowData(cls, minor):
1040
    """Return the `drbdsetup show` data for a minor.
1041

1042
    """
1043
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "show"])
1044
    if result.failed:
1045
      logging.error("Can't display the drbd config: %s - %s",
1046
                    result.fail_reason, result.output)
1047
      return None
1048
    return result.stdout
1049

    
1050
  @classmethod
1051
  def _GetDevInfo(cls, out):
1052
    """Parse details about a given DRBD minor.
1053

1054
    This return, if available, the local backing device (as a path)
1055
    and the local and remote (ip, port) information from a string
1056
    containing the output of the `drbdsetup show` command as returned
1057
    by _GetShowData.
1058

1059
    """
1060
    data = {}
1061
    if not out:
1062
      return data
1063

    
1064
    bnf = cls._GetShowParser()
1065
    # run pyparse
1066

    
1067
    try:
1068
      results = bnf.parseString(out)
1069
    except pyp.ParseException, err:
1070
      _ThrowError("Can't parse drbdsetup show output: %s", str(err))
1071

    
1072
    # and massage the results into our desired format
1073
    for section in results:
1074
      sname = section[0]
1075
      if sname == "_this_host":
1076
        for lst in section[1:]:
1077
          if lst[0] == "disk":
1078
            data["local_dev"] = lst[1]
1079
          elif lst[0] == "meta-disk":
1080
            data["meta_dev"] = lst[1]
1081
            data["meta_index"] = lst[2]
1082
          elif lst[0] == "address":
1083
            data["local_addr"] = tuple(lst[1:])
1084
      elif sname == "_remote_host":
1085
        for lst in section[1:]:
1086
          if lst[0] == "address":
1087
            data["remote_addr"] = tuple(lst[1:])
1088
    return data
1089

    
1090
  def _MatchesLocal(self, info):
1091
    """Test if our local config matches with an existing device.
1092

1093
    The parameter should be as returned from `_GetDevInfo()`. This
1094
    method tests if our local backing device is the same as the one in
1095
    the info parameter, in effect testing if we look like the given
1096
    device.
1097

1098
    """
1099
    if self._children:
1100
      backend, meta = self._children
1101
    else:
1102
      backend = meta = None
1103

    
1104
    if backend is not None:
1105
      retval = ("local_dev" in info and info["local_dev"] == backend.dev_path)
1106
    else:
1107
      retval = ("local_dev" not in info)
1108

    
1109
    if meta is not None:
1110
      retval = retval and ("meta_dev" in info and
1111
                           info["meta_dev"] == meta.dev_path)
1112
      retval = retval and ("meta_index" in info and
1113
                           info["meta_index"] == 0)
1114
    else:
1115
      retval = retval and ("meta_dev" not in info and
1116
                           "meta_index" not in info)
1117
    return retval
1118

    
1119
  def _MatchesNet(self, info):
1120
    """Test if our network config matches with an existing device.
1121

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

1127
    """
1128
    if (((self._lhost is None and not ("local_addr" in info)) and
1129
         (self._rhost is None and not ("remote_addr" in info)))):
1130
      return True
1131

    
1132
    if self._lhost is None:
1133
      return False
1134

    
1135
    if not ("local_addr" in info and
1136
            "remote_addr" in info):
1137
      return False
1138

    
1139
    retval = (info["local_addr"] == (self._lhost, self._lport))
1140
    retval = (retval and
1141
              info["remote_addr"] == (self._rhost, self._rport))
1142
    return retval
1143

    
1144
  @classmethod
1145
  def _AssembleLocal(cls, minor, backend, meta, size):
1146
    """Configure the local part of a DRBD device.
1147

1148
    """
1149
    args = ["drbdsetup", cls._DevPath(minor), "disk",
1150
            backend, meta, "0",
1151
            "-e", "detach",
1152
            "--create-device"]
1153
    if size:
1154
      args.extend(["-d", "%sm" % size])
1155
    result = utils.RunCmd(args)
1156
    if result.failed:
1157
      _ThrowError("drbd%d: can't attach local disk: %s", minor, result.output)
1158

    
1159
  @classmethod
1160
  def _AssembleNet(cls, minor, net_info, protocol,
1161
                   dual_pri=False, hmac=None, secret=None):
1162
    """Configure the network part of the device.
1163

1164
    """
1165
    lhost, lport, rhost, rport = net_info
1166
    if None in net_info:
1167
      # we don't want network connection and actually want to make
1168
      # sure its shutdown
1169
      cls._ShutdownNet(minor)
1170
      return
1171

    
1172
    # Workaround for a race condition. When DRBD is doing its dance to
1173
    # establish a connection with its peer, it also sends the
1174
    # synchronization speed over the wire. In some cases setting the
1175
    # sync speed only after setting up both sides can race with DRBD
1176
    # connecting, hence we set it here before telling DRBD anything
1177
    # about its peer.
1178
    cls._SetMinorSyncSpeed(minor, constants.SYNC_SPEED)
1179

    
1180
    args = ["drbdsetup", cls._DevPath(minor), "net",
1181
            "%s:%s" % (lhost, lport), "%s:%s" % (rhost, rport), protocol,
1182
            "-A", "discard-zero-changes",
1183
            "-B", "consensus",
1184
            "--create-device",
1185
            ]
1186
    if dual_pri:
1187
      args.append("-m")
1188
    if hmac and secret:
1189
      args.extend(["-a", hmac, "-x", secret])
1190
    result = utils.RunCmd(args)
1191
    if result.failed:
1192
      _ThrowError("drbd%d: can't setup network: %s - %s",
1193
                  minor, result.fail_reason, result.output)
1194

    
1195
    timeout = time.time() + 10
1196
    ok = False
1197
    while time.time() < timeout:
1198
      info = cls._GetDevInfo(cls._GetShowData(minor))
1199
      if not "local_addr" in info or not "remote_addr" in info:
1200
        time.sleep(1)
1201
        continue
1202
      if (info["local_addr"] != (lhost, lport) or
1203
          info["remote_addr"] != (rhost, rport)):
1204
        time.sleep(1)
1205
        continue
1206
      ok = True
1207
      break
1208
    if not ok:
1209
      _ThrowError("drbd%d: timeout while configuring network", minor)
1210

    
1211
  def AddChildren(self, devices):
1212
    """Add a disk to the DRBD device.
1213

1214
    """
1215
    if self.minor is None:
1216
      _ThrowError("drbd%d: can't attach to dbrd8 during AddChildren",
1217
                  self._aminor)
1218
    if len(devices) != 2:
1219
      _ThrowError("drbd%d: need two devices for AddChildren", self.minor)
1220
    info = self._GetDevInfo(self._GetShowData(self.minor))
1221
    if "local_dev" in info:
1222
      _ThrowError("drbd%d: already attached to a local disk", self.minor)
1223
    backend, meta = devices
1224
    if backend.dev_path is None or meta.dev_path is None:
1225
      _ThrowError("drbd%d: children not ready during AddChildren", self.minor)
1226
    backend.Open()
1227
    meta.Open()
1228
    self._CheckMetaSize(meta.dev_path)
1229
    self._InitMeta(self._FindUnusedMinor(), meta.dev_path)
1230

    
1231
    self._AssembleLocal(self.minor, backend.dev_path, meta.dev_path, self.size)
1232
    self._children = devices
1233

    
1234
  def RemoveChildren(self, devices):
1235
    """Detach the drbd device from local storage.
1236

1237
    """
1238
    if self.minor is None:
1239
      _ThrowError("drbd%d: can't attach to drbd8 during RemoveChildren",
1240
                  self._aminor)
1241
    # early return if we don't actually have backing storage
1242
    info = self._GetDevInfo(self._GetShowData(self.minor))
1243
    if "local_dev" not in info:
1244
      return
1245
    if len(self._children) != 2:
1246
      _ThrowError("drbd%d: we don't have two children: %s", self.minor,
1247
                  self._children)
1248
    if self._children.count(None) == 2: # we don't actually have children :)
1249
      logging.warning("drbd%d: requested detach while detached", self.minor)
1250
      return
1251
    if len(devices) != 2:
1252
      _ThrowError("drbd%d: we need two children in RemoveChildren", self.minor)
1253
    for child, dev in zip(self._children, devices):
1254
      if dev != child.dev_path:
1255
        _ThrowError("drbd%d: mismatch in local storage (%s != %s) in"
1256
                    " RemoveChildren", self.minor, dev, child.dev_path)
1257

    
1258
    self._ShutdownLocal(self.minor)
1259
    self._children = []
1260

    
1261
  @classmethod
1262
  def _SetMinorSyncSpeed(cls, minor, kbytes):
1263
    """Set the speed of the DRBD syncer.
1264

1265
    This is the low-level implementation.
1266

1267
    @type minor: int
1268
    @param minor: the drbd minor whose settings we change
1269
    @type kbytes: int
1270
    @param kbytes: the speed in kbytes/second
1271
    @rtype: boolean
1272
    @return: the success of the operation
1273

1274
    """
1275
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "syncer",
1276
                           "-r", "%d" % kbytes, "--create-device"])
1277
    if result.failed:
1278
      logging.error("Can't change syncer rate: %s - %s",
1279
                    result.fail_reason, result.output)
1280
    return not result.failed
1281

    
1282
  def SetSyncSpeed(self, kbytes):
1283
    """Set the speed of the DRBD syncer.
1284

1285
    @type kbytes: int
1286
    @param kbytes: the speed in kbytes/second
1287
    @rtype: boolean
1288
    @return: the success of the operation
1289

1290
    """
1291
    if self.minor is None:
1292
      logging.info("Not attached during SetSyncSpeed")
1293
      return False
1294
    children_result = super(DRBD8, self).SetSyncSpeed(kbytes)
1295
    return self._SetMinorSyncSpeed(self.minor, kbytes) and children_result
1296

    
1297
  def GetProcStatus(self):
1298
    """Return device data from /proc.
1299

1300
    """
1301
    if self.minor is None:
1302
      _ThrowError("drbd%d: GetStats() called while not attached", self._aminor)
1303
    proc_info = self._MassageProcData(self._GetProcData())
1304
    if self.minor not in proc_info:
1305
      _ThrowError("drbd%d: can't find myself in /proc", self.minor)
1306
    return DRBD8Status(proc_info[self.minor])
1307

    
1308
  def GetSyncStatus(self):
1309
    """Returns the sync status of the device.
1310

1311

1312
    If sync_percent is None, it means all is ok
1313
    If estimated_time is None, it means we can't estimate
1314
    the time needed, otherwise it's the time left in seconds.
1315

1316

1317
    We set the is_degraded parameter to True on two conditions:
1318
    network not connected or local disk missing.
1319

1320
    We compute the ldisk parameter based on whether we have a local
1321
    disk or not.
1322

1323
    @rtype: tuple
1324
    @return: (sync_percent, estimated_time, is_degraded, ldisk)
1325

1326
    """
1327
    if self.minor is None and not self.Attach():
1328
      _ThrowError("drbd%d: can't Attach() in GetSyncStatus", self._aminor)
1329
    stats = self.GetProcStatus()
1330
    ldisk = not stats.is_disk_uptodate
1331
    is_degraded = not stats.is_connected
1332
    return stats.sync_percent, stats.est_time, is_degraded or ldisk, ldisk
1333

    
1334
  def Open(self, force=False):
1335
    """Make the local state primary.
1336

1337
    If the 'force' parameter is given, the '-o' option is passed to
1338
    drbdsetup. Since this is a potentially dangerous operation, the
1339
    force flag should be only given after creation, when it actually
1340
    is mandatory.
1341

1342
    """
1343
    if self.minor is None and not self.Attach():
1344
      logging.error("DRBD cannot attach to a device during open")
1345
      return False
1346
    cmd = ["drbdsetup", self.dev_path, "primary"]
1347
    if force:
1348
      cmd.append("-o")
1349
    result = utils.RunCmd(cmd)
1350
    if result.failed:
1351
      _ThrowError("drbd%d: can't make drbd device primary: %s", self.minor,
1352
                  result.output)
1353

    
1354
  def Close(self):
1355
    """Make the local state secondary.
1356

1357
    This will, of course, fail if the device is in use.
1358

1359
    """
1360
    if self.minor is None and not self.Attach():
1361
      _ThrowError("drbd%d: can't Attach() in Close()", self._aminor)
1362
    result = utils.RunCmd(["drbdsetup", self.dev_path, "secondary"])
1363
    if result.failed:
1364
      _ThrowError("drbd%d: can't switch drbd device to secondary: %s",
1365
                  self.minor, result.output)
1366

    
1367
  def DisconnectNet(self):
1368
    """Removes network configuration.
1369

1370
    This method shutdowns the network side of the device.
1371

1372
    The method will wait up to a hardcoded timeout for the device to
1373
    go into standalone after the 'disconnect' command before
1374
    re-configuring it, as sometimes it takes a while for the
1375
    disconnect to actually propagate and thus we might issue a 'net'
1376
    command while the device is still connected. If the device will
1377
    still be attached to the network and we time out, we raise an
1378
    exception.
1379

1380
    """
1381
    if self.minor is None:
1382
      _ThrowError("drbd%d: disk not attached in re-attach net", self._aminor)
1383

    
1384
    if None in (self._lhost, self._lport, self._rhost, self._rport):
1385
      _ThrowError("drbd%d: DRBD disk missing network info in"
1386
                  " DisconnectNet()", self.minor)
1387

    
1388
    ever_disconnected = _IgnoreError(self._ShutdownNet, self.minor)
1389
    timeout_limit = time.time() + self._NET_RECONFIG_TIMEOUT
1390
    sleep_time = 0.100 # we start the retry time at 100 milliseconds
1391
    while time.time() < timeout_limit:
1392
      status = self.GetProcStatus()
1393
      if status.is_standalone:
1394
        break
1395
      # retry the disconnect, it seems possible that due to a
1396
      # well-time disconnect on the peer, my disconnect command might
1397
      # be ignored and forgotten
1398
      ever_disconnected = _IgnoreError(self._ShutdownNet, self.minor) or \
1399
                          ever_disconnected
1400
      time.sleep(sleep_time)
1401
      sleep_time = min(2, sleep_time * 1.5)
1402

    
1403
    if not status.is_standalone:
1404
      if ever_disconnected:
1405
        msg = ("drbd%d: device did not react to the"
1406
               " 'disconnect' command in a timely manner")
1407
      else:
1408
        msg = "drbd%d: can't shutdown network, even after multiple retries"
1409
      _ThrowError(msg, self.minor)
1410

    
1411
    reconfig_time = time.time() - timeout_limit + self._NET_RECONFIG_TIMEOUT
1412
    if reconfig_time > 15: # hardcoded alert limit
1413
      logging.info("drbd%d: DisconnectNet: detach took %.3f seconds",
1414
                   self.minor, reconfig_time)
1415

    
1416
  def AttachNet(self, multimaster):
1417
    """Reconnects the network.
1418

1419
    This method connects the network side of the device with a
1420
    specified multi-master flag. The device needs to be 'Standalone'
1421
    but have valid network configuration data.
1422

1423
    Args:
1424
      - multimaster: init the network in dual-primary mode
1425

1426
    """
1427
    if self.minor is None:
1428
      _ThrowError("drbd%d: device not attached in AttachNet", self._aminor)
1429

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

    
1433
    status = self.GetProcStatus()
1434

    
1435
    if not status.is_standalone:
1436
      _ThrowError("drbd%d: device is not standalone in AttachNet", self.minor)
1437

    
1438
    self._AssembleNet(self.minor,
1439
                      (self._lhost, self._lport, self._rhost, self._rport),
1440
                      constants.DRBD_NET_PROTOCOL, dual_pri=multimaster,
1441
                      hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1442

    
1443
  def Attach(self):
1444
    """Check if our minor is configured.
1445

1446
    This doesn't do any device configurations - it only checks if the
1447
    minor is in a state different from Unconfigured.
1448

1449
    Note that this function will not change the state of the system in
1450
    any way (except in case of side-effects caused by reading from
1451
    /proc).
1452

1453
    """
1454
    used_devs = self.GetUsedDevs()
1455
    if self._aminor in used_devs:
1456
      minor = self._aminor
1457
    else:
1458
      minor = None
1459

    
1460
    self._SetFromMinor(minor)
1461
    return minor is not None
1462

    
1463
  def Assemble(self):
1464
    """Assemble the drbd.
1465

1466
    Method:
1467
      - if we have a configured device, we try to ensure that it matches
1468
        our config
1469
      - if not, we create it from zero
1470

1471
    """
1472
    super(DRBD8, self).Assemble()
1473

    
1474
    self.Attach()
1475
    if self.minor is None:
1476
      # local device completely unconfigured
1477
      self._FastAssemble()
1478
    else:
1479
      # we have to recheck the local and network status and try to fix
1480
      # the device
1481
      self._SlowAssemble()
1482

    
1483
  def _SlowAssemble(self):
1484
    """Assembles the DRBD device from a (partially) configured device.
1485

1486
    In case of partially attached (local device matches but no network
1487
    setup), we perform the network attach. If successful, we re-test
1488
    the attach if can return success.
1489

1490
    """
1491
    net_data = (self._lhost, self._lport, self._rhost, self._rport)
1492
    for minor in (self._aminor,):
1493
      info = self._GetDevInfo(self._GetShowData(minor))
1494
      match_l = self._MatchesLocal(info)
1495
      match_r = self._MatchesNet(info)
1496

    
1497
      if match_l and match_r:
1498
        # everything matches
1499
        break
1500

    
1501
      if match_l and not match_r and "local_addr" not in info:
1502
        # disk matches, but not attached to network, attach and recheck
1503
        self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
1504
                          hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1505
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1506
          break
1507
        else:
1508
          _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
1509
                      " show' disagrees", minor)
1510

    
1511
      if match_r and "local_dev" not in info:
1512
        # no local disk, but network attached and it matches
1513
        self._AssembleLocal(minor, self._children[0].dev_path,
1514
                            self._children[1].dev_path, self.size)
1515
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1516
          break
1517
        else:
1518
          _ThrowError("drbd%d: disk attach successful, but 'drbdsetup"
1519
                      " show' disagrees", minor)
1520

    
1521
      # this case must be considered only if we actually have local
1522
      # storage, i.e. not in diskless mode, because all diskless
1523
      # devices are equal from the point of view of local
1524
      # configuration
1525
      if (match_l and "local_dev" in info and
1526
          not match_r and "local_addr" in info):
1527
        # strange case - the device network part points to somewhere
1528
        # else, even though its local storage is ours; as we own the
1529
        # drbd space, we try to disconnect from the remote peer and
1530
        # reconnect to our correct one
1531
        try:
1532
          self._ShutdownNet(minor)
1533
        except errors.BlockDeviceError, err:
1534
          _ThrowError("drbd%d: device has correct local storage, wrong"
1535
                      " remote peer and is unable to disconnect in order"
1536
                      " to attach to the correct peer: %s", minor, str(err))
1537
        # note: _AssembleNet also handles the case when we don't want
1538
        # local storage (i.e. one or more of the _[lr](host|port) is
1539
        # None)
1540
        self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
1541
                          hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1542
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1543
          break
1544
        else:
1545
          _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
1546
                      " show' disagrees", minor)
1547

    
1548
    else:
1549
      minor = None
1550

    
1551
    self._SetFromMinor(minor)
1552
    if minor is None:
1553
      _ThrowError("drbd%d: cannot activate, unknown or unhandled reason",
1554
                  self._aminor)
1555

    
1556
  def _FastAssemble(self):
1557
    """Assemble the drbd device from zero.
1558

1559
    This is run when in Assemble we detect our minor is unused.
1560

1561
    """
1562
    minor = self._aminor
1563
    if self._children and self._children[0] and self._children[1]:
1564
      self._AssembleLocal(minor, self._children[0].dev_path,
1565
                          self._children[1].dev_path, self.size)
1566
    if self._lhost and self._lport and self._rhost and self._rport:
1567
      self._AssembleNet(minor,
1568
                        (self._lhost, self._lport, self._rhost, self._rport),
1569
                        constants.DRBD_NET_PROTOCOL,
1570
                        hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1571
    self._SetFromMinor(minor)
1572

    
1573
  @classmethod
1574
  def _ShutdownLocal(cls, minor):
1575
    """Detach from the local device.
1576

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

1580
    """
1581
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "detach"])
1582
    if result.failed:
1583
      _ThrowError("drbd%d: can't detach local disk: %s", minor, result.output)
1584

    
1585
  @classmethod
1586
  def _ShutdownNet(cls, minor):
1587
    """Disconnect from the remote peer.
1588

1589
    This fails if we don't have a local device.
1590

1591
    """
1592
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disconnect"])
1593
    if result.failed:
1594
      _ThrowError("drbd%d: can't shutdown network: %s", minor, result.output)
1595

    
1596
  @classmethod
1597
  def _ShutdownAll(cls, minor):
1598
    """Deactivate the device.
1599

1600
    This will, of course, fail if the device is in use.
1601

1602
    """
1603
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "down"])
1604
    if result.failed:
1605
      _ThrowError("drbd%d: can't shutdown drbd device: %s",
1606
                  minor, result.output)
1607

    
1608
  def Shutdown(self):
1609
    """Shutdown the DRBD device.
1610

1611
    """
1612
    if self.minor is None and not self.Attach():
1613
      logging.info("drbd%d: not attached during Shutdown()", self._aminor)
1614
      return
1615
    minor = self.minor
1616
    self.minor = None
1617
    self.dev_path = None
1618
    self._ShutdownAll(minor)
1619

    
1620
  def Remove(self):
1621
    """Stub remove for DRBD devices.
1622

1623
    """
1624
    self.Shutdown()
1625

    
1626
  @classmethod
1627
  def Create(cls, unique_id, children, size):
1628
    """Create a new DRBD8 device.
1629

1630
    Since DRBD devices are not created per se, just assembled, this
1631
    function only initializes the metadata.
1632

1633
    """
1634
    if len(children) != 2:
1635
      raise errors.ProgrammerError("Invalid setup for the drbd device")
1636
    # check that the minor is unused
1637
    aminor = unique_id[4]
1638
    proc_info = cls._MassageProcData(cls._GetProcData())
1639
    if aminor in proc_info:
1640
      status = DRBD8Status(proc_info[aminor])
1641
      in_use = status.is_in_use
1642
    else:
1643
      in_use = False
1644
    if in_use:
1645
      _ThrowError("drbd%d: minor is already in use at Create() time", aminor)
1646
    meta = children[1]
1647
    meta.Assemble()
1648
    if not meta.Attach():
1649
      _ThrowError("drbd%d: can't attach to meta device '%s'",
1650
                  aminor, meta)
1651
    cls._CheckMetaSize(meta.dev_path)
1652
    cls._InitMeta(aminor, meta.dev_path)
1653
    return cls(unique_id, children, size)
1654

    
1655
  def Grow(self, amount):
1656
    """Resize the DRBD device and its backing storage.
1657

1658
    """
1659
    if self.minor is None:
1660
      _ThrowError("drbd%d: Grow called while not attached", self._aminor)
1661
    if len(self._children) != 2 or None in self._children:
1662
      _ThrowError("drbd%d: cannot grow diskless device", self.minor)
1663
    self._children[0].Grow(amount)
1664
    result = utils.RunCmd(["drbdsetup", self.dev_path, "resize", "-s",
1665
                           "%dm" % (self.size + amount)])
1666
    if result.failed:
1667
      _ThrowError("drbd%d: resize failed: %s", self.minor, result.output)
1668

    
1669

    
1670
class FileStorage(BlockDev):
1671
  """File device.
1672

1673
  This class represents the a file storage backend device.
1674

1675
  The unique_id for the file device is a (file_driver, file_path) tuple.
1676

1677
  """
1678
  def __init__(self, unique_id, children, size):
1679
    """Initalizes a file device backend.
1680

1681
    """
1682
    if children:
1683
      raise errors.BlockDeviceError("Invalid setup for file device")
1684
    super(FileStorage, self).__init__(unique_id, children, size)
1685
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1686
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1687
    self.driver = unique_id[0]
1688
    self.dev_path = unique_id[1]
1689
    self.Attach()
1690

    
1691
  def Assemble(self):
1692
    """Assemble the device.
1693

1694
    Checks whether the file device exists, raises BlockDeviceError otherwise.
1695

1696
    """
1697
    if not os.path.exists(self.dev_path):
1698
      _ThrowError("File device '%s' does not exist" % self.dev_path)
1699

    
1700
  def Shutdown(self):
1701
    """Shutdown the device.
1702

1703
    This is a no-op for the file type, as we don't deactivate
1704
    the file on shutdown.
1705

1706
    """
1707
    pass
1708

    
1709
  def Open(self, force=False):
1710
    """Make the device ready for I/O.
1711

1712
    This is a no-op for the file type.
1713

1714
    """
1715
    pass
1716

    
1717
  def Close(self):
1718
    """Notifies that the device will no longer be used for I/O.
1719

1720
    This is a no-op for the file type.
1721

1722
    """
1723
    pass
1724

    
1725
  def Remove(self):
1726
    """Remove the file backing the block device.
1727

1728
    @rtype: boolean
1729
    @return: True if the removal was successful
1730

1731
    """
1732
    try:
1733
      os.remove(self.dev_path)
1734
    except OSError, err:
1735
      if err.errno != errno.ENOENT:
1736
        _ThrowError("Can't remove file '%s': %s", self.dev_path, err)
1737

    
1738
  def Attach(self):
1739
    """Attach to an existing file.
1740

1741
    Check if this file already exists.
1742

1743
    @rtype: boolean
1744
    @return: True if file exists
1745

1746
    """
1747
    self.attached = os.path.exists(self.dev_path)
1748
    return self.attached
1749

    
1750
  def GetActualSize(self):
1751
    """Return the actual disk size.
1752

1753
    @note: the device needs to be active when this is called
1754

1755
    """
1756
    assert self.attached, "BlockDevice not attached in GetActualSize()"
1757
    try:
1758
      st = os.stat(self.dev_path)
1759
      return st.st_size
1760
    except OSError, err:
1761
      _ThrowError("Can't stat %s: %s", self.dev_path, err)
1762

    
1763
  @classmethod
1764
  def Create(cls, unique_id, children, size):
1765
    """Create a new file.
1766

1767
    @param size: the size of file in MiB
1768

1769
    @rtype: L{bdev.FileStorage}
1770
    @return: an instance of FileStorage
1771

1772
    """
1773
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1774
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1775
    dev_path = unique_id[1]
1776
    if os.path.exists(dev_path):
1777
      _ThrowError("File already existing: %s", dev_path)
1778
    try:
1779
      f = open(dev_path, 'w')
1780
      f.truncate(size * 1024 * 1024)
1781
      f.close()
1782
    except IOError, err:
1783
      _ThrowError("Error in file creation: %", str(err))
1784

    
1785
    return FileStorage(unique_id, children, size)
1786

    
1787

    
1788
DEV_MAP = {
1789
  constants.LD_LV: LogicalVolume,
1790
  constants.LD_DRBD8: DRBD8,
1791
  constants.LD_FILE: FileStorage,
1792
  }
1793

    
1794

    
1795
def FindDevice(dev_type, unique_id, children, size):
1796
  """Search for an existing, assembled device.
1797

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

1801
  """
1802
  if dev_type not in DEV_MAP:
1803
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1804
  device = DEV_MAP[dev_type](unique_id, children, size)
1805
  if not device.attached:
1806
    return None
1807
  return device
1808

    
1809

    
1810
def Assemble(dev_type, unique_id, children, size):
1811
  """Try to attach or assemble an existing device.
1812

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

1816
  """
1817
  if dev_type not in DEV_MAP:
1818
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1819
  device = DEV_MAP[dev_type](unique_id, children, size)
1820
  device.Assemble()
1821
  return device
1822

    
1823

    
1824
def Create(dev_type, unique_id, children, size):
1825
  """Create a device.
1826

1827
  """
1828
  if dev_type not in DEV_MAP:
1829
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1830
  device = DEV_MAP[dev_type].Create(unique_id, children, size)
1831
  return device