Statistics
| Branch: | Tag: | Revision:

root / lib / bdev.py @ 38256320

History | View | Annotate | Download (57 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
    storeage. 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 __repr__(self):
281
    return ("<%s: unique_id: %s, children: %s, %s:%s, %s>" %
282
            (self.__class__, self.unique_id, self._children,
283
             self.major, self.minor, self.dev_path))
284

    
285

    
286
class LogicalVolume(BlockDev):
287
  """Logical Volume block device.
288

289
  """
290
  def __init__(self, unique_id, children, size):
291
    """Attaches to a LV device.
292

293
    The unique_id is a tuple (vg_name, lv_name)
294

295
    """
296
    super(LogicalVolume, self).__init__(unique_id, children, size)
297
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
298
      raise ValueError("Invalid configuration data %s" % str(unique_id))
299
    self._vg_name, self._lv_name = unique_id
300
    self.dev_path = "/dev/%s/%s" % (self._vg_name, self._lv_name)
301
    self._degraded = True
302
    self.major = self.minor = self.pe_size = self.stripe_count = None
303
    self.Attach()
304

    
305
  @classmethod
306
  def Create(cls, unique_id, children, size):
307
    """Create a new logical volume.
308

309
    """
310
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
311
      raise errors.ProgrammerError("Invalid configuration data %s" %
312
                                   str(unique_id))
313
    vg_name, lv_name = unique_id
314
    pvs_info = cls.GetPVInfo(vg_name)
315
    if not pvs_info:
316
      _ThrowError("Can't compute PV info for vg %s", vg_name)
317
    pvs_info.sort()
318
    pvs_info.reverse()
319

    
320
    pvlist = [ pv[1] for pv in pvs_info ]
321
    free_size = sum([ pv[0] for pv in pvs_info ])
322
    current_pvs = len(pvlist)
323
    stripes = min(current_pvs, constants.LVM_STRIPECOUNT)
324

    
325
    # The size constraint should have been checked from the master before
326
    # calling the create function.
327
    if free_size < size:
328
      _ThrowError("Not enough free space: required %s,"
329
                  " available %s", size, free_size)
330
    cmd = ["lvcreate", "-L%dm" % size, "-n%s" % lv_name]
331
    # If the free space is not well distributed, we won't be able to
332
    # create an optimally-striped volume; in that case, we want to try
333
    # with N, N-1, ..., 2, and finally 1 (non-stripped) number of
334
    # stripes
335
    for stripes_arg in range(stripes, 0, -1):
336
      result = utils.RunCmd(cmd + ["-i%d" % stripes_arg] + [vg_name] + pvlist)
337
      if not result.failed:
338
        break
339
    if result.failed:
340
      _ThrowError("LV create failed (%s): %s",
341
                  result.fail_reason, result.output)
342
    return LogicalVolume(unique_id, children, size)
343

    
344
  @staticmethod
345
  def GetPVInfo(vg_name):
346
    """Get the free space info for PVs in a volume group.
347

348
    @param vg_name: the volume group name
349

350
    @rtype: list
351
    @return: list of tuples (free_space, name) with free_space in mebibytes
352

353
    """
354
    command = ["pvs", "--noheadings", "--nosuffix", "--units=m",
355
               "-opv_name,vg_name,pv_free,pv_attr", "--unbuffered",
356
               "--separator=:"]
357
    result = utils.RunCmd(command)
358
    if result.failed:
359
      logging.error("Can't get the PV information: %s - %s",
360
                    result.fail_reason, result.output)
361
      return None
362
    data = []
363
    for line in result.stdout.splitlines():
364
      fields = line.strip().split(':')
365
      if len(fields) != 4:
366
        logging.error("Can't parse pvs output: line '%s'", line)
367
        return None
368
      # skip over pvs from another vg or ones which are not allocatable
369
      if fields[1] != vg_name or fields[3][0] != 'a':
370
        continue
371
      data.append((float(fields[2]), fields[0]))
372

    
373
    return data
374

    
375
  def Remove(self):
376
    """Remove this logical volume.
377

378
    """
379
    if not self.minor and not self.Attach():
380
      # the LV does not exist
381
      return
382
    result = utils.RunCmd(["lvremove", "-f", "%s/%s" %
383
                           (self._vg_name, self._lv_name)])
384
    if result.failed:
385
      _ThrowError("Can't lvremove: %s - %s", result.fail_reason, result.output)
386

    
387
  def Rename(self, new_id):
388
    """Rename this logical volume.
389

390
    """
391
    if not isinstance(new_id, (tuple, list)) or len(new_id) != 2:
392
      raise errors.ProgrammerError("Invalid new logical id '%s'" % new_id)
393
    new_vg, new_name = new_id
394
    if new_vg != self._vg_name:
395
      raise errors.ProgrammerError("Can't move a logical volume across"
396
                                   " volume groups (from %s to to %s)" %
397
                                   (self._vg_name, new_vg))
398
    result = utils.RunCmd(["lvrename", new_vg, self._lv_name, new_name])
399
    if result.failed:
400
      _ThrowError("Failed to rename the logical volume: %s", result.output)
401
    self._lv_name = new_name
402
    self.dev_path = "/dev/%s/%s" % (self._vg_name, self._lv_name)
403

    
404
  def Attach(self):
405
    """Attach to an existing LV.
406

407
    This method will try to see if an existing and active LV exists
408
    which matches our name. If so, its major/minor will be
409
    recorded.
410

411
    """
412
    self.attached = False
413
    result = utils.RunCmd(["lvs", "--noheadings", "--separator=,",
414
                           "--units=m", "--nosuffix",
415
                           "-olv_attr,lv_kernel_major,lv_kernel_minor,"
416
                           "vg_extent_size,stripes", self.dev_path])
417
    if result.failed:
418
      logging.error("Can't find LV %s: %s, %s",
419
                    self.dev_path, result.fail_reason, result.output)
420
      return False
421
    # the output can (and will) have multiple lines for multi-segment
422
    # LVs, as the 'stripes' parameter is a segment one, so we take
423
    # only the last entry, which is the one we're interested in; note
424
    # that with LVM2 anyway the 'stripes' value must be constant
425
    # across segments, so this is a no-op actually
426
    out = result.stdout.splitlines()
427
    if not out: # totally empty result? splitlines() returns at least
428
                # one line for any non-empty string
429
      logging.error("Can't parse LVS output, no lines? Got '%s'", str(out))
430
      return False
431
    out = out[-1].strip().rstrip(',')
432
    out = out.split(",")
433
    if len(out) != 5:
434
      logging.error("Can't parse LVS output, len(%s) != 5", str(out))
435
      return False
436

    
437
    status, major, minor, pe_size, stripes = out
438
    if len(status) != 6:
439
      logging.error("lvs lv_attr is not 6 characters (%s)", status)
440
      return False
441

    
442
    try:
443
      major = int(major)
444
      minor = int(minor)
445
    except ValueError, err:
446
      logging.error("lvs major/minor cannot be parsed: %s", str(err))
447

    
448
    try:
449
      pe_size = int(float(pe_size))
450
    except (TypeError, ValueError), err:
451
      logging.error("Can't parse vg extent size: %s", err)
452
      return False
453

    
454
    try:
455
      stripes = int(stripes)
456
    except (TypeError, ValueError), err:
457
      logging.error("Can't parse the number of stripes: %s", err)
458
      return False
459

    
460
    self.major = major
461
    self.minor = minor
462
    self.pe_size = pe_size
463
    self.stripe_count = stripes
464
    self._degraded = status[0] == 'v' # virtual volume, i.e. doesn't backing
465
                                      # storage
466
    self.attached = True
467
    return True
468

    
469
  def Assemble(self):
470
    """Assemble the device.
471

472
    We alway run `lvchange -ay` on the LV to ensure it's active before
473
    use, as there were cases when xenvg was not active after boot
474
    (also possibly after disk issues).
475

476
    """
477
    result = utils.RunCmd(["lvchange", "-ay", self.dev_path])
478
    if result.failed:
479
      _ThrowError("Can't activate lv %s: %s", self.dev_path, result.output)
480

    
481
  def Shutdown(self):
482
    """Shutdown the device.
483

484
    This is a no-op for the LV device type, as we don't deactivate the
485
    volumes on shutdown.
486

487
    """
488
    pass
489

    
490
  def GetSyncStatus(self):
491
    """Returns the sync status of the device.
492

493
    If this device is a mirroring device, this function returns the
494
    status of the mirror.
495

496
    For logical volumes, sync_percent and estimated_time are always
497
    None (no recovery in progress, as we don't handle the mirrored LV
498
    case). The is_degraded parameter is the inverse of the ldisk
499
    parameter.
500

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

507
    The status was already read in Attach, so we just return it.
508

509
    @rtype: tuple
510
    @return: (sync_percent, estimated_time, is_degraded, ldisk)
511

512
    """
513
    return None, None, self._degraded, self._degraded
514

    
515
  def Open(self, force=False):
516
    """Make the device ready for I/O.
517

518
    This is a no-op for the LV device type.
519

520
    """
521
    pass
522

    
523
  def Close(self):
524
    """Notifies that the device will no longer be used for I/O.
525

526
    This is a no-op for the LV device type.
527

528
    """
529
    pass
530

    
531
  def Snapshot(self, size):
532
    """Create a snapshot copy of an lvm block device.
533

534
    """
535
    snap_name = self._lv_name + ".snap"
536

    
537
    # remove existing snapshot if found
538
    snap = LogicalVolume((self._vg_name, snap_name), None, size)
539
    _IgnoreError(snap.Remove)
540

    
541
    pvs_info = self.GetPVInfo(self._vg_name)
542
    if not pvs_info:
543
      _ThrowError("Can't compute PV info for vg %s", self._vg_name)
544
    pvs_info.sort()
545
    pvs_info.reverse()
546
    free_size, pv_name = pvs_info[0]
547
    if free_size < size:
548
      _ThrowError("Not enough free space: required %s,"
549
                  " available %s", size, free_size)
550

    
551
    result = utils.RunCmd(["lvcreate", "-L%dm" % size, "-s",
552
                           "-n%s" % snap_name, self.dev_path])
553
    if result.failed:
554
      _ThrowError("command: %s error: %s - %s",
555
                  result.cmd, result.fail_reason, result.output)
556

    
557
    return snap_name
558

    
559
  def SetInfo(self, text):
560
    """Update metadata with info text.
561

562
    """
563
    BlockDev.SetInfo(self, text)
564

    
565
    # Replace invalid characters
566
    text = re.sub('^[^A-Za-z0-9_+.]', '_', text)
567
    text = re.sub('[^-A-Za-z0-9_+.]', '_', text)
568

    
569
    # Only up to 128 characters are allowed
570
    text = text[:128]
571

    
572
    result = utils.RunCmd(["lvchange", "--addtag", text,
573
                           self.dev_path])
574
    if result.failed:
575
      _ThrowError("Command: %s error: %s - %s", result.cmd, result.fail_reason,
576
                  result.output)
577

    
578
  def Grow(self, amount):
579
    """Grow the logical volume.
580

581
    """
582
    if self.pe_size is None or self.stripe_count is None:
583
      if not self.Attach():
584
        _ThrowError("Can't attach to LV during Grow()")
585
    full_stripe_size = self.pe_size * self.stripe_count
586
    rest = amount % full_stripe_size
587
    if rest != 0:
588
      amount += full_stripe_size - rest
589
    # we try multiple algorithms since the 'best' ones might not have
590
    # space available in the right place, but later ones might (since
591
    # they have less constraints); also note that only recent LVM
592
    # supports 'cling'
593
    for alloc_policy in "contiguous", "cling", "normal":
594
      result = utils.RunCmd(["lvextend", "--alloc", alloc_policy,
595
                             "-L", "+%dm" % amount, self.dev_path])
596
      if not result.failed:
597
        return
598
    _ThrowError("Can't grow LV %s: %s", self.dev_path, result.output)
599

    
600

    
601
class DRBD8Status(object):
602
  """A DRBD status representation class.
603

604
  Note that this doesn't support unconfigured devices (cs:Unconfigured).
605

606
  """
607
  UNCONF_RE = re.compile(r"\s*[0-9]+:\s*cs:Unconfigured$")
608
  LINE_RE = re.compile(r"\s*[0-9]+:\s*cs:(\S+)\s+(?:st|ro):([^/]+)/(\S+)"
609
                       "\s+ds:([^/]+)/(\S+)\s+.*$")
610
  SYNC_RE = re.compile(r"^.*\ssync'ed:\s*([0-9.]+)%.*"
611
                       "\sfinish: ([0-9]+):([0-9]+):([0-9]+)\s.*$")
612

    
613
  CS_UNCONFIGURED = "Unconfigured"
614
  CS_STANDALONE = "StandAlone"
615
  CS_WFCONNECTION = "WFConnection"
616
  CS_WFREPORTPARAMS = "WFReportParams"
617
  CS_CONNECTED = "Connected"
618
  CS_STARTINGSYNCS = "StartingSyncS"
619
  CS_STARTINGSYNCT = "StartingSyncT"
620
  CS_WFBITMAPS = "WFBitMapS"
621
  CS_WFBITMAPT = "WFBitMapT"
622
  CS_WFSYNCUUID = "WFSyncUUID"
623
  CS_SYNCSOURCE = "SyncSource"
624
  CS_SYNCTARGET = "SyncTarget"
625
  CS_PAUSEDSYNCS = "PausedSyncS"
626
  CS_PAUSEDSYNCT = "PausedSyncT"
627
  CSET_SYNC = frozenset([
628
    CS_WFREPORTPARAMS,
629
    CS_STARTINGSYNCS,
630
    CS_STARTINGSYNCT,
631
    CS_WFBITMAPS,
632
    CS_WFBITMAPT,
633
    CS_WFSYNCUUID,
634
    CS_SYNCSOURCE,
635
    CS_SYNCTARGET,
636
    CS_PAUSEDSYNCS,
637
    CS_PAUSEDSYNCT,
638
    ])
639

    
640
  DS_DISKLESS = "Diskless"
641
  DS_ATTACHING = "Attaching" # transient state
642
  DS_FAILED = "Failed" # transient state, next: diskless
643
  DS_NEGOTIATING = "Negotiating" # transient state
644
  DS_INCONSISTENT = "Inconsistent" # while syncing or after creation
645
  DS_OUTDATED = "Outdated"
646
  DS_DUNKNOWN = "DUnknown" # shown for peer disk when not connected
647
  DS_CONSISTENT = "Consistent"
648
  DS_UPTODATE = "UpToDate" # normal state
649

    
650
  RO_PRIMARY = "Primary"
651
  RO_SECONDARY = "Secondary"
652
  RO_UNKNOWN = "Unknown"
653

    
654
  def __init__(self, procline):
655
    u = self.UNCONF_RE.match(procline)
656
    if u:
657
      self.cstatus = self.CS_UNCONFIGURED
658
      self.lrole = self.rrole = self.ldisk = self.rdisk = None
659
    else:
660
      m = self.LINE_RE.match(procline)
661
      if not m:
662
        raise errors.BlockDeviceError("Can't parse input data '%s'" % procline)
663
      self.cstatus = m.group(1)
664
      self.lrole = m.group(2)
665
      self.rrole = m.group(3)
666
      self.ldisk = m.group(4)
667
      self.rdisk = m.group(5)
668

    
669
    # end reading of data from the LINE_RE or UNCONF_RE
670

    
671
    self.is_standalone = self.cstatus == self.CS_STANDALONE
672
    self.is_wfconn = self.cstatus == self.CS_WFCONNECTION
673
    self.is_connected = self.cstatus == self.CS_CONNECTED
674
    self.is_primary = self.lrole == self.RO_PRIMARY
675
    self.is_secondary = self.lrole == self.RO_SECONDARY
676
    self.peer_primary = self.rrole == self.RO_PRIMARY
677
    self.peer_secondary = self.rrole == self.RO_SECONDARY
678
    self.both_primary = self.is_primary and self.peer_primary
679
    self.both_secondary = self.is_secondary and self.peer_secondary
680

    
681
    self.is_diskless = self.ldisk == self.DS_DISKLESS
682
    self.is_disk_uptodate = self.ldisk == self.DS_UPTODATE
683

    
684
    self.is_in_resync = self.cstatus in self.CSET_SYNC
685
    self.is_in_use = self.cstatus != self.CS_UNCONFIGURED
686

    
687
    m = self.SYNC_RE.match(procline)
688
    if m:
689
      self.sync_percent = float(m.group(1))
690
      hours = int(m.group(2))
691
      minutes = int(m.group(3))
692
      seconds = int(m.group(4))
693
      self.est_time = hours * 3600 + minutes * 60 + seconds
694
    else:
695
      # we have (in this if branch) no percent information, but if
696
      # we're resyncing we need to 'fake' a sync percent information,
697
      # as this is how cmdlib determines if it makes sense to wait for
698
      # resyncing or not
699
      if self.is_in_resync:
700
        self.sync_percent = 0
701
      else:
702
        self.sync_percent = None
703
      self.est_time = None
704

    
705

    
706
class BaseDRBD(BlockDev):
707
  """Base DRBD class.
708

709
  This class contains a few bits of common functionality between the
710
  0.7 and 8.x versions of DRBD.
711

712
  """
713
  _VERSION_RE = re.compile(r"^version: (\d+)\.(\d+)\.(\d+)"
714
                           r" \(api:(\d+)/proto:(\d+)(?:-(\d+))?\)")
715

    
716
  _DRBD_MAJOR = 147
717
  _ST_UNCONFIGURED = "Unconfigured"
718
  _ST_WFCONNECTION = "WFConnection"
719
  _ST_CONNECTED = "Connected"
720

    
721
  _STATUS_FILE = "/proc/drbd"
722

    
723
  @staticmethod
724
  def _GetProcData(filename=_STATUS_FILE):
725
    """Return data from /proc/drbd.
726

727
    """
728
    try:
729
      stat = open(filename, "r")
730
      try:
731
        data = stat.read().splitlines()
732
      finally:
733
        stat.close()
734
    except EnvironmentError, err:
735
      if err.errno == errno.ENOENT:
736
        _ThrowError("The file %s cannot be opened, check if the module"
737
                    " is loaded (%s)", filename, str(err))
738
      else:
739
        _ThrowError("Can't read the DRBD proc file %s: %s", filename, str(err))
740
    if not data:
741
      _ThrowError("Can't read any data from %s", filename)
742
    return data
743

    
744
  @staticmethod
745
  def _MassageProcData(data):
746
    """Transform the output of _GetProdData into a nicer form.
747

748
    @return: a dictionary of minor: joined lines from /proc/drbd
749
        for that minor
750

751
    """
752
    lmatch = re.compile("^ *([0-9]+):.*$")
753
    results = {}
754
    old_minor = old_line = None
755
    for line in data:
756
      lresult = lmatch.match(line)
757
      if lresult is not None:
758
        if old_minor is not None:
759
          results[old_minor] = old_line
760
        old_minor = int(lresult.group(1))
761
        old_line = line
762
      else:
763
        if old_minor is not None:
764
          old_line += " " + line.strip()
765
    # add last line
766
    if old_minor is not None:
767
      results[old_minor] = old_line
768
    return results
769

    
770
  @classmethod
771
  def _GetVersion(cls):
772
    """Return the DRBD version.
773

774
    This will return a dict with keys:
775
      - k_major
776
      - k_minor
777
      - k_point
778
      - api
779
      - proto
780
      - proto2 (only on drbd > 8.2.X)
781

782
    """
783
    proc_data = cls._GetProcData()
784
    first_line = proc_data[0].strip()
785
    version = cls._VERSION_RE.match(first_line)
786
    if not version:
787
      raise errors.BlockDeviceError("Can't parse DRBD version from '%s'" %
788
                                    first_line)
789

    
790
    values = version.groups()
791
    retval = {'k_major': int(values[0]),
792
              'k_minor': int(values[1]),
793
              'k_point': int(values[2]),
794
              'api': int(values[3]),
795
              'proto': int(values[4]),
796
             }
797
    if values[5] is not None:
798
      retval['proto2'] = values[5]
799

    
800
    return retval
801

    
802
  @staticmethod
803
  def _DevPath(minor):
804
    """Return the path to a drbd device for a given minor.
805

806
    """
807
    return "/dev/drbd%d" % minor
808

    
809
  @classmethod
810
  def GetUsedDevs(cls):
811
    """Compute the list of used DRBD devices.
812

813
    """
814
    data = cls._GetProcData()
815

    
816
    used_devs = {}
817
    valid_line = re.compile("^ *([0-9]+): cs:([^ ]+).*$")
818
    for line in data:
819
      match = valid_line.match(line)
820
      if not match:
821
        continue
822
      minor = int(match.group(1))
823
      state = match.group(2)
824
      if state == cls._ST_UNCONFIGURED:
825
        continue
826
      used_devs[minor] = state, line
827

    
828
    return used_devs
829

    
830
  def _SetFromMinor(self, minor):
831
    """Set our parameters based on the given minor.
832

833
    This sets our minor variable and our dev_path.
834

835
    """
836
    if minor is None:
837
      self.minor = self.dev_path = None
838
      self.attached = False
839
    else:
840
      self.minor = minor
841
      self.dev_path = self._DevPath(minor)
842
      self.attached = True
843

    
844
  @staticmethod
845
  def _CheckMetaSize(meta_device):
846
    """Check if the given meta device looks like a valid one.
847

848
    This currently only check the size, which must be around
849
    128MiB.
850

851
    """
852
    result = utils.RunCmd(["blockdev", "--getsize", meta_device])
853
    if result.failed:
854
      _ThrowError("Failed to get device size: %s - %s",
855
                  result.fail_reason, result.output)
856
    try:
857
      sectors = int(result.stdout)
858
    except ValueError:
859
      _ThrowError("Invalid output from blockdev: '%s'", result.stdout)
860
    bytes = sectors * 512
861
    if bytes < 128 * 1024 * 1024: # less than 128MiB
862
      _ThrowError("Meta device too small (%.2fMib)", (bytes / 1024 / 1024))
863
    # the maximum *valid* size of the meta device when living on top
864
    # of LVM is hard to compute: it depends on the number of stripes
865
    # and the PE size; e.g. a 2-stripe, 64MB PE will result in a 128MB
866
    # (normal size), but an eight-stripe 128MB PE will result in a 1GB
867
    # size meta device; as such, we restrict it to 1GB (a little bit
868
    # too generous, but making assumptions about PE size is hard)
869
    if bytes > 1024 * 1024 * 1024:
870
      _ThrowError("Meta device too big (%.2fMiB)", (bytes / 1024 / 1024))
871

    
872
  def Rename(self, new_id):
873
    """Rename a device.
874

875
    This is not supported for drbd devices.
876

877
    """
878
    raise errors.ProgrammerError("Can't rename a drbd device")
879

    
880

    
881
class DRBD8(BaseDRBD):
882
  """DRBD v8.x block device.
883

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

888
  The unique_id for the drbd device is the (local_ip, local_port,
889
  remote_ip, remote_port) tuple, and it must have two children: the
890
  data device and the meta_device. The meta device is checked for
891
  valid size and is zeroed on create.
892

893
  """
894
  _MAX_MINORS = 255
895
  _PARSE_SHOW = None
896

    
897
  # timeout constants
898
  _NET_RECONFIG_TIMEOUT = 60
899

    
900
  def __init__(self, unique_id, children, size):
901
    if children and children.count(None) > 0:
902
      children = []
903
    super(DRBD8, self).__init__(unique_id, children, size)
904
    self.major = self._DRBD_MAJOR
905
    version = self._GetVersion()
906
    if version['k_major'] != 8 :
907
      _ThrowError("Mismatch in DRBD kernel version and requested ganeti"
908
                  " usage: kernel is %s.%s, ganeti wants 8.x",
909
                  version['k_major'], version['k_minor'])
910

    
911
    if len(children) not in (0, 2):
912
      raise ValueError("Invalid configuration data %s" % str(children))
913
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 6:
914
      raise ValueError("Invalid configuration data %s" % str(unique_id))
915
    (self._lhost, self._lport,
916
     self._rhost, self._rport,
917
     self._aminor, self._secret) = unique_id
918
    if (self._lhost is not None and self._lhost == self._rhost and
919
        self._lport == self._rport):
920
      raise ValueError("Invalid configuration data, same local/remote %s" %
921
                       (unique_id,))
922
    self.Attach()
923

    
924
  @classmethod
925
  def _InitMeta(cls, minor, dev_path):
926
    """Initialize a meta device.
927

928
    This will not work if the given minor is in use.
929

930
    """
931
    result = utils.RunCmd(["drbdmeta", "--force", cls._DevPath(minor),
932
                           "v08", dev_path, "0", "create-md"])
933
    if result.failed:
934
      _ThrowError("Can't initialize meta device: %s", result.output)
935

    
936
  @classmethod
937
  def _FindUnusedMinor(cls):
938
    """Find an unused DRBD device.
939

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

943
    """
944
    data = cls._GetProcData()
945

    
946
    unused_line = re.compile("^ *([0-9]+): cs:Unconfigured$")
947
    used_line = re.compile("^ *([0-9]+): cs:")
948
    highest = None
949
    for line in data:
950
      match = unused_line.match(line)
951
      if match:
952
        return int(match.group(1))
953
      match = used_line.match(line)
954
      if match:
955
        minor = int(match.group(1))
956
        highest = max(highest, minor)
957
    if highest is None: # there are no minors in use at all
958
      return 0
959
    if highest >= cls._MAX_MINORS:
960
      logging.error("Error: no free drbd minors!")
961
      raise errors.BlockDeviceError("Can't find a free DRBD minor")
962
    return highest + 1
963

    
964
  @classmethod
965
  def _GetShowParser(cls):
966
    """Return a parser for `drbd show` output.
967

968
    This will either create or return an already-create parser for the
969
    output of the command `drbd show`.
970

971
    """
972
    if cls._PARSE_SHOW is not None:
973
      return cls._PARSE_SHOW
974

    
975
    # pyparsing setup
976
    lbrace = pyp.Literal("{").suppress()
977
    rbrace = pyp.Literal("}").suppress()
978
    semi = pyp.Literal(";").suppress()
979
    # this also converts the value to an int
980
    number = pyp.Word(pyp.nums).setParseAction(lambda s, l, t: int(t[0]))
981

    
982
    comment = pyp.Literal ("#") + pyp.Optional(pyp.restOfLine)
983
    defa = pyp.Literal("_is_default").suppress()
984
    dbl_quote = pyp.Literal('"').suppress()
985

    
986
    keyword = pyp.Word(pyp.alphanums + '-')
987

    
988
    # value types
989
    value = pyp.Word(pyp.alphanums + '_-/.:')
990
    quoted = dbl_quote + pyp.CharsNotIn('"') + dbl_quote
991
    addr_type = (pyp.Optional(pyp.Literal("ipv4")).suppress() +
992
                 pyp.Optional(pyp.Literal("ipv6")).suppress())
993
    addr_port = (addr_type + pyp.Word(pyp.nums + '.') +
994
                 pyp.Literal(':').suppress() + number)
995
    # meta device, extended syntax
996
    meta_value = ((value ^ quoted) + pyp.Literal('[').suppress() +
997
                  number + pyp.Word(']').suppress())
998
    # device name, extended syntax
999
    device_value = pyp.Literal("minor").suppress() + number
1000

    
1001
    # a statement
1002
    stmt = (~rbrace + keyword + ~lbrace +
1003
            pyp.Optional(addr_port ^ value ^ quoted ^ meta_value ^
1004
                         device_value) +
1005
            pyp.Optional(defa) + semi +
1006
            pyp.Optional(pyp.restOfLine).suppress())
1007

    
1008
    # an entire section
1009
    section_name = pyp.Word(pyp.alphas + '_')
1010
    section = section_name + lbrace + pyp.ZeroOrMore(pyp.Group(stmt)) + rbrace
1011

    
1012
    bnf = pyp.ZeroOrMore(pyp.Group(section ^ stmt))
1013
    bnf.ignore(comment)
1014

    
1015
    cls._PARSE_SHOW = bnf
1016

    
1017
    return bnf
1018

    
1019
  @classmethod
1020
  def _GetShowData(cls, minor):
1021
    """Return the `drbdsetup show` data for a minor.
1022

1023
    """
1024
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "show"])
1025
    if result.failed:
1026
      logging.error("Can't display the drbd config: %s - %s",
1027
                    result.fail_reason, result.output)
1028
      return None
1029
    return result.stdout
1030

    
1031
  @classmethod
1032
  def _GetDevInfo(cls, out):
1033
    """Parse details about a given DRBD minor.
1034

1035
    This return, if available, the local backing device (as a path)
1036
    and the local and remote (ip, port) information from a string
1037
    containing the output of the `drbdsetup show` command as returned
1038
    by _GetShowData.
1039

1040
    """
1041
    data = {}
1042
    if not out:
1043
      return data
1044

    
1045
    bnf = cls._GetShowParser()
1046
    # run pyparse
1047

    
1048
    try:
1049
      results = bnf.parseString(out)
1050
    except pyp.ParseException, err:
1051
      _ThrowError("Can't parse drbdsetup show output: %s", str(err))
1052

    
1053
    # and massage the results into our desired format
1054
    for section in results:
1055
      sname = section[0]
1056
      if sname == "_this_host":
1057
        for lst in section[1:]:
1058
          if lst[0] == "disk":
1059
            data["local_dev"] = lst[1]
1060
          elif lst[0] == "meta-disk":
1061
            data["meta_dev"] = lst[1]
1062
            data["meta_index"] = lst[2]
1063
          elif lst[0] == "address":
1064
            data["local_addr"] = tuple(lst[1:])
1065
      elif sname == "_remote_host":
1066
        for lst in section[1:]:
1067
          if lst[0] == "address":
1068
            data["remote_addr"] = tuple(lst[1:])
1069
    return data
1070

    
1071
  def _MatchesLocal(self, info):
1072
    """Test if our local config matches with an existing device.
1073

1074
    The parameter should be as returned from `_GetDevInfo()`. This
1075
    method tests if our local backing device is the same as the one in
1076
    the info parameter, in effect testing if we look like the given
1077
    device.
1078

1079
    """
1080
    if self._children:
1081
      backend, meta = self._children
1082
    else:
1083
      backend = meta = None
1084

    
1085
    if backend is not None:
1086
      retval = ("local_dev" in info and info["local_dev"] == backend.dev_path)
1087
    else:
1088
      retval = ("local_dev" not in info)
1089

    
1090
    if meta is not None:
1091
      retval = retval and ("meta_dev" in info and
1092
                           info["meta_dev"] == meta.dev_path)
1093
      retval = retval and ("meta_index" in info and
1094
                           info["meta_index"] == 0)
1095
    else:
1096
      retval = retval and ("meta_dev" not in info and
1097
                           "meta_index" not in info)
1098
    return retval
1099

    
1100
  def _MatchesNet(self, info):
1101
    """Test if our network config matches with an existing device.
1102

1103
    The parameter should be as returned from `_GetDevInfo()`. This
1104
    method tests if our network configuration is the same as the one
1105
    in the info parameter, in effect testing if we look like the given
1106
    device.
1107

1108
    """
1109
    if (((self._lhost is None and not ("local_addr" in info)) and
1110
         (self._rhost is None and not ("remote_addr" in info)))):
1111
      return True
1112

    
1113
    if self._lhost is None:
1114
      return False
1115

    
1116
    if not ("local_addr" in info and
1117
            "remote_addr" in info):
1118
      return False
1119

    
1120
    retval = (info["local_addr"] == (self._lhost, self._lport))
1121
    retval = (retval and
1122
              info["remote_addr"] == (self._rhost, self._rport))
1123
    return retval
1124

    
1125
  @classmethod
1126
  def _AssembleLocal(cls, minor, backend, meta, size):
1127
    """Configure the local part of a DRBD device.
1128

1129
    """
1130
    args = ["drbdsetup", cls._DevPath(minor), "disk",
1131
            backend, meta, "0",
1132
            "-d", "%sm" % size,
1133
            "-e", "detach",
1134
            "--create-device"]
1135
    result = utils.RunCmd(args)
1136
    if result.failed:
1137
      _ThrowError("drbd%d: can't attach local disk: %s", minor, result.output)
1138

    
1139
  @classmethod
1140
  def _AssembleNet(cls, minor, net_info, protocol,
1141
                   dual_pri=False, hmac=None, secret=None):
1142
    """Configure the network part of the device.
1143

1144
    """
1145
    lhost, lport, rhost, rport = net_info
1146
    if None in net_info:
1147
      # we don't want network connection and actually want to make
1148
      # sure its shutdown
1149
      cls._ShutdownNet(minor)
1150
      return
1151

    
1152
    # Workaround for a race condition. When DRBD is doing its dance to
1153
    # establish a connection with its peer, it also sends the
1154
    # synchronization speed over the wire. In some cases setting the
1155
    # sync speed only after setting up both sides can race with DRBD
1156
    # connecting, hence we set it here before telling DRBD anything
1157
    # about its peer.
1158
    cls._SetMinorSyncSpeed(minor, constants.SYNC_SPEED)
1159

    
1160
    args = ["drbdsetup", cls._DevPath(minor), "net",
1161
            "%s:%s" % (lhost, lport), "%s:%s" % (rhost, rport), protocol,
1162
            "-A", "discard-zero-changes",
1163
            "-B", "consensus",
1164
            "--create-device",
1165
            ]
1166
    if dual_pri:
1167
      args.append("-m")
1168
    if hmac and secret:
1169
      args.extend(["-a", hmac, "-x", secret])
1170
    result = utils.RunCmd(args)
1171
    if result.failed:
1172
      _ThrowError("drbd%d: can't setup network: %s - %s",
1173
                  minor, result.fail_reason, result.output)
1174

    
1175
    timeout = time.time() + 10
1176
    ok = False
1177
    while time.time() < timeout:
1178
      info = cls._GetDevInfo(cls._GetShowData(minor))
1179
      if not "local_addr" in info or not "remote_addr" in info:
1180
        time.sleep(1)
1181
        continue
1182
      if (info["local_addr"] != (lhost, lport) or
1183
          info["remote_addr"] != (rhost, rport)):
1184
        time.sleep(1)
1185
        continue
1186
      ok = True
1187
      break
1188
    if not ok:
1189
      _ThrowError("drbd%d: timeout while configuring network", minor)
1190

    
1191
  def AddChildren(self, devices):
1192
    """Add a disk to the DRBD device.
1193

1194
    """
1195
    if self.minor is None:
1196
      _ThrowError("drbd%d: can't attach to dbrd8 during AddChildren",
1197
                  self._aminor)
1198
    if len(devices) != 2:
1199
      _ThrowError("drbd%d: need two devices for AddChildren", self.minor)
1200
    info = self._GetDevInfo(self._GetShowData(self.minor))
1201
    if "local_dev" in info:
1202
      _ThrowError("drbd%d: already attached to a local disk", self.minor)
1203
    backend, meta = devices
1204
    if backend.dev_path is None or meta.dev_path is None:
1205
      _ThrowError("drbd%d: children not ready during AddChildren", self.minor)
1206
    backend.Open()
1207
    meta.Open()
1208
    self._CheckMetaSize(meta.dev_path)
1209
    self._InitMeta(self._FindUnusedMinor(), meta.dev_path)
1210

    
1211
    self._AssembleLocal(self.minor, backend.dev_path, meta.dev_path, self.size)
1212
    self._children = devices
1213

    
1214
  def RemoveChildren(self, devices):
1215
    """Detach the drbd device from local storage.
1216

1217
    """
1218
    if self.minor is None:
1219
      _ThrowError("drbd%d: can't attach to drbd8 during RemoveChildren",
1220
                  self._aminor)
1221
    # early return if we don't actually have backing storage
1222
    info = self._GetDevInfo(self._GetShowData(self.minor))
1223
    if "local_dev" not in info:
1224
      return
1225
    if len(self._children) != 2:
1226
      _ThrowError("drbd%d: we don't have two children: %s", self.minor,
1227
                  self._children)
1228
    if self._children.count(None) == 2: # we don't actually have children :)
1229
      logging.warning("drbd%d: requested detach while detached", self.minor)
1230
      return
1231
    if len(devices) != 2:
1232
      _ThrowError("drbd%d: we need two children in RemoveChildren", self.minor)
1233
    for child, dev in zip(self._children, devices):
1234
      if dev != child.dev_path:
1235
        _ThrowError("drbd%d: mismatch in local storage (%s != %s) in"
1236
                    " RemoveChildren", self.minor, dev, child.dev_path)
1237

    
1238
    self._ShutdownLocal(self.minor)
1239
    self._children = []
1240

    
1241
  @classmethod
1242
  def _SetMinorSyncSpeed(cls, minor, kbytes):
1243
    """Set the speed of the DRBD syncer.
1244

1245
    This is the low-level implementation.
1246

1247
    @type minor: int
1248
    @param minor: the drbd minor whose settings we change
1249
    @type kbytes: int
1250
    @param kbytes: the speed in kbytes/second
1251
    @rtype: boolean
1252
    @return: the success of the operation
1253

1254
    """
1255
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "syncer",
1256
                           "-r", "%d" % kbytes, "--create-device"])
1257
    if result.failed:
1258
      logging.error("Can't change syncer rate: %s - %s",
1259
                    result.fail_reason, result.output)
1260
    return not result.failed
1261

    
1262
  def SetSyncSpeed(self, kbytes):
1263
    """Set the speed of the DRBD syncer.
1264

1265
    @type kbytes: int
1266
    @param kbytes: the speed in kbytes/second
1267
    @rtype: boolean
1268
    @return: the success of the operation
1269

1270
    """
1271
    if self.minor is None:
1272
      logging.info("Not attached during SetSyncSpeed")
1273
      return False
1274
    children_result = super(DRBD8, self).SetSyncSpeed(kbytes)
1275
    return self._SetMinorSyncSpeed(self.minor, kbytes) and children_result
1276

    
1277
  def GetProcStatus(self):
1278
    """Return device data from /proc.
1279

1280
    """
1281
    if self.minor is None:
1282
      _ThrowError("drbd%d: GetStats() called while not attached", self._aminor)
1283
    proc_info = self._MassageProcData(self._GetProcData())
1284
    if self.minor not in proc_info:
1285
      _ThrowError("drbd%d: can't find myself in /proc", self.minor)
1286
    return DRBD8Status(proc_info[self.minor])
1287

    
1288
  def GetSyncStatus(self):
1289
    """Returns the sync status of the device.
1290

1291

1292
    If sync_percent is None, it means all is ok
1293
    If estimated_time is None, it means we can't esimate
1294
    the time needed, otherwise it's the time left in seconds.
1295

1296

1297
    We set the is_degraded parameter to True on two conditions:
1298
    network not connected or local disk missing.
1299

1300
    We compute the ldisk parameter based on wheter we have a local
1301
    disk or not.
1302

1303
    @rtype: tuple
1304
    @return: (sync_percent, estimated_time, is_degraded, ldisk)
1305

1306
    """
1307
    if self.minor is None and not self.Attach():
1308
      _ThrowError("drbd%d: can't Attach() in GetSyncStatus", self._aminor)
1309
    stats = self.GetProcStatus()
1310
    ldisk = not stats.is_disk_uptodate
1311
    is_degraded = not stats.is_connected
1312
    return stats.sync_percent, stats.est_time, is_degraded or ldisk, ldisk
1313

    
1314
  def Open(self, force=False):
1315
    """Make the local state primary.
1316

1317
    If the 'force' parameter is given, the '-o' option is passed to
1318
    drbdsetup. Since this is a potentially dangerous operation, the
1319
    force flag should be only given after creation, when it actually
1320
    is mandatory.
1321

1322
    """
1323
    if self.minor is None and not self.Attach():
1324
      logging.error("DRBD cannot attach to a device during open")
1325
      return False
1326
    cmd = ["drbdsetup", self.dev_path, "primary"]
1327
    if force:
1328
      cmd.append("-o")
1329
    result = utils.RunCmd(cmd)
1330
    if result.failed:
1331
      _ThrowError("drbd%d: can't make drbd device primary: %s", self.minor,
1332
                  result.output)
1333

    
1334
  def Close(self):
1335
    """Make the local state secondary.
1336

1337
    This will, of course, fail if the device is in use.
1338

1339
    """
1340
    if self.minor is None and not self.Attach():
1341
      _ThrowError("drbd%d: can't Attach() in Close()", self._aminor)
1342
    result = utils.RunCmd(["drbdsetup", self.dev_path, "secondary"])
1343
    if result.failed:
1344
      _ThrowError("drbd%d: can't switch drbd device to secondary: %s",
1345
                  self.minor, result.output)
1346

    
1347
  def DisconnectNet(self):
1348
    """Removes network configuration.
1349

1350
    This method shutdowns the network side of the device.
1351

1352
    The method will wait up to a hardcoded timeout for the device to
1353
    go into standalone after the 'disconnect' command before
1354
    re-configuring it, as sometimes it takes a while for the
1355
    disconnect to actually propagate and thus we might issue a 'net'
1356
    command while the device is still connected. If the device will
1357
    still be attached to the network and we time out, we raise an
1358
    exception.
1359

1360
    """
1361
    if self.minor is None:
1362
      _ThrowError("drbd%d: disk not attached in re-attach net", self._aminor)
1363

    
1364
    if None in (self._lhost, self._lport, self._rhost, self._rport):
1365
      _ThrowError("drbd%d: DRBD disk missing network info in"
1366
                  " DisconnectNet()", self.minor)
1367

    
1368
    ever_disconnected = _IgnoreError(self._ShutdownNet, self.minor)
1369
    timeout_limit = time.time() + self._NET_RECONFIG_TIMEOUT
1370
    sleep_time = 0.100 # we start the retry time at 100 miliseconds
1371
    while time.time() < timeout_limit:
1372
      status = self.GetProcStatus()
1373
      if status.is_standalone:
1374
        break
1375
      # retry the disconnect, it seems possible that due to a
1376
      # well-time disconnect on the peer, my disconnect command might
1377
      # be ingored and forgotten
1378
      ever_disconnected = _IgnoreError(self._ShutdownNet, self.minor) or \
1379
                          ever_disconnected
1380
      time.sleep(sleep_time)
1381
      sleep_time = min(2, sleep_time * 1.5)
1382

    
1383
    if not status.is_standalone:
1384
      if ever_disconnected:
1385
        msg = ("drbd%d: device did not react to the"
1386
               " 'disconnect' command in a timely manner")
1387
      else:
1388
        msg = "drbd%d: can't shutdown network, even after multiple retries"
1389
      _ThrowError(msg, self.minor)
1390

    
1391
    reconfig_time = time.time() - timeout_limit + self._NET_RECONFIG_TIMEOUT
1392
    if reconfig_time > 15: # hardcoded alert limit
1393
      logging.info("drbd%d: DisconnectNet: detach took %.3f seconds",
1394
                   self.minor, reconfig_time)
1395

    
1396
  def AttachNet(self, multimaster):
1397
    """Reconnects the network.
1398

1399
    This method connects the network side of the device with a
1400
    specified multi-master flag. The device needs to be 'Standalone'
1401
    but have valid network configuration data.
1402

1403
    Args:
1404
      - multimaster: init the network in dual-primary mode
1405

1406
    """
1407
    if self.minor is None:
1408
      _ThrowError("drbd%d: device not attached in AttachNet", self._aminor)
1409

    
1410
    if None in (self._lhost, self._lport, self._rhost, self._rport):
1411
      _ThrowError("drbd%d: missing network info in AttachNet()", self.minor)
1412

    
1413
    status = self.GetProcStatus()
1414

    
1415
    if not status.is_standalone:
1416
      _ThrowError("drbd%d: device is not standalone in AttachNet", self.minor)
1417

    
1418
    self._AssembleNet(self.minor,
1419
                      (self._lhost, self._lport, self._rhost, self._rport),
1420
                      constants.DRBD_NET_PROTOCOL, dual_pri=multimaster,
1421
                      hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1422

    
1423
  def Attach(self):
1424
    """Check if our minor is configured.
1425

1426
    This doesn't do any device configurations - it only checks if the
1427
    minor is in a state different from Unconfigured.
1428

1429
    Note that this function will not change the state of the system in
1430
    any way (except in case of side-effects caused by reading from
1431
    /proc).
1432

1433
    """
1434
    used_devs = self.GetUsedDevs()
1435
    if self._aminor in used_devs:
1436
      minor = self._aminor
1437
    else:
1438
      minor = None
1439

    
1440
    self._SetFromMinor(minor)
1441
    return minor is not None
1442

    
1443
  def Assemble(self):
1444
    """Assemble the drbd.
1445

1446
    Method:
1447
      - if we have a configured device, we try to ensure that it matches
1448
        our config
1449
      - if not, we create it from zero
1450

1451
    """
1452
    super(DRBD8, self).Assemble()
1453

    
1454
    self.Attach()
1455
    if self.minor is None:
1456
      # local device completely unconfigured
1457
      self._FastAssemble()
1458
    else:
1459
      # we have to recheck the local and network status and try to fix
1460
      # the device
1461
      self._SlowAssemble()
1462

    
1463
  def _SlowAssemble(self):
1464
    """Assembles the DRBD device from a (partially) configured device.
1465

1466
    In case of partially attached (local device matches but no network
1467
    setup), we perform the network attach. If successful, we re-test
1468
    the attach if can return success.
1469

1470
    """
1471
    net_data = (self._lhost, self._lport, self._rhost, self._rport)
1472
    for minor in (self._aminor,):
1473
      info = self._GetDevInfo(self._GetShowData(minor))
1474
      match_l = self._MatchesLocal(info)
1475
      match_r = self._MatchesNet(info)
1476

    
1477
      if match_l and match_r:
1478
        # everything matches
1479
        break
1480

    
1481
      if match_l and not match_r and "local_addr" not in info:
1482
        # disk matches, but not attached to network, attach and recheck
1483
        self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
1484
                          hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1485
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1486
          break
1487
        else:
1488
          _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
1489
                      " show' disagrees", minor)
1490

    
1491
      if match_r and "local_dev" not in info:
1492
        # no local disk, but network attached and it matches
1493
        self._AssembleLocal(minor, self._children[0].dev_path,
1494
                            self._children[1].dev_path, self.size)
1495
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1496
          break
1497
        else:
1498
          _ThrowError("drbd%d: disk attach successful, but 'drbdsetup"
1499
                      " show' disagrees", minor)
1500

    
1501
      # this case must be considered only if we actually have local
1502
      # storage, i.e. not in diskless mode, because all diskless
1503
      # devices are equal from the point of view of local
1504
      # configuration
1505
      if (match_l and "local_dev" in info and
1506
          not match_r and "local_addr" in info):
1507
        # strange case - the device network part points to somewhere
1508
        # else, even though its local storage is ours; as we own the
1509
        # drbd space, we try to disconnect from the remote peer and
1510
        # reconnect to our correct one
1511
        try:
1512
          self._ShutdownNet(minor)
1513
        except errors.BlockDeviceError, err:
1514
          _ThrowError("drbd%d: device has correct local storage, wrong"
1515
                      " remote peer and is unable to disconnect in order"
1516
                      " to attach to the correct peer: %s", minor, str(err))
1517
        # note: _AssembleNet also handles the case when we don't want
1518
        # local storage (i.e. one or more of the _[lr](host|port) is
1519
        # None)
1520
        self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
1521
                          hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1522
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1523
          break
1524
        else:
1525
          _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
1526
                      " show' disagrees", minor)
1527

    
1528
    else:
1529
      minor = None
1530

    
1531
    self._SetFromMinor(minor)
1532
    if minor is None:
1533
      _ThrowError("drbd%d: cannot activate, unknown or unhandled reason",
1534
                  self._aminor)
1535

    
1536
  def _FastAssemble(self):
1537
    """Assemble the drbd device from zero.
1538

1539
    This is run when in Assemble we detect our minor is unused.
1540

1541
    """
1542
    minor = self._aminor
1543
    if self._children and self._children[0] and self._children[1]:
1544
      self._AssembleLocal(minor, self._children[0].dev_path,
1545
                          self._children[1].dev_path, self.size)
1546
    if self._lhost and self._lport and self._rhost and self._rport:
1547
      self._AssembleNet(minor,
1548
                        (self._lhost, self._lport, self._rhost, self._rport),
1549
                        constants.DRBD_NET_PROTOCOL,
1550
                        hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1551
    self._SetFromMinor(minor)
1552

    
1553
  @classmethod
1554
  def _ShutdownLocal(cls, minor):
1555
    """Detach from the local device.
1556

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

1560
    """
1561
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "detach"])
1562
    if result.failed:
1563
      _ThrowError("drbd%d: can't detach local disk: %s", minor, result.output)
1564

    
1565
  @classmethod
1566
  def _ShutdownNet(cls, minor):
1567
    """Disconnect from the remote peer.
1568

1569
    This fails if we don't have a local device.
1570

1571
    """
1572
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disconnect"])
1573
    if result.failed:
1574
      _ThrowError("drbd%d: can't shutdown network: %s", minor, result.output)
1575

    
1576
  @classmethod
1577
  def _ShutdownAll(cls, minor):
1578
    """Deactivate the device.
1579

1580
    This will, of course, fail if the device is in use.
1581

1582
    """
1583
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "down"])
1584
    if result.failed:
1585
      _ThrowError("drbd%d: can't shutdown drbd device: %s",
1586
                  minor, result.output)
1587

    
1588
  def Shutdown(self):
1589
    """Shutdown the DRBD device.
1590

1591
    """
1592
    if self.minor is None and not self.Attach():
1593
      logging.info("drbd%d: not attached during Shutdown()", self._aminor)
1594
      return
1595
    minor = self.minor
1596
    self.minor = None
1597
    self.dev_path = None
1598
    self._ShutdownAll(minor)
1599

    
1600
  def Remove(self):
1601
    """Stub remove for DRBD devices.
1602

1603
    """
1604
    self.Shutdown()
1605

    
1606
  @classmethod
1607
  def Create(cls, unique_id, children, size):
1608
    """Create a new DRBD8 device.
1609

1610
    Since DRBD devices are not created per se, just assembled, this
1611
    function only initializes the metadata.
1612

1613
    """
1614
    if len(children) != 2:
1615
      raise errors.ProgrammerError("Invalid setup for the drbd device")
1616
    # check that the minor is unused
1617
    aminor = unique_id[4]
1618
    proc_info = cls._MassageProcData(cls._GetProcData())
1619
    if aminor in proc_info:
1620
      status = DRBD8Status(proc_info[aminor])
1621
      in_use = status.is_in_use
1622
    else:
1623
      in_use = False
1624
    if in_use:
1625
      _ThrowError("drbd%d: minor is already in use at Create() time", aminor)
1626
    meta = children[1]
1627
    meta.Assemble()
1628
    if not meta.Attach():
1629
      _ThrowError("drbd%d: can't attach to meta device '%s'",
1630
                  aminor, meta)
1631
    cls._CheckMetaSize(meta.dev_path)
1632
    cls._InitMeta(aminor, meta.dev_path)
1633
    return cls(unique_id, children, size)
1634

    
1635
  def Grow(self, amount):
1636
    """Resize the DRBD device and its backing storage.
1637

1638
    """
1639
    if self.minor is None:
1640
      _ThrowError("drbd%d: Grow called while not attached", self._aminor)
1641
    if len(self._children) != 2 or None in self._children:
1642
      _ThrowError("drbd%d: cannot grow diskless device", self.minor)
1643
    self._children[0].Grow(amount)
1644
    result = utils.RunCmd(["drbdsetup", self.dev_path, "resize", "-s",
1645
                           "%dm" % (self.size + amount)])
1646
    if result.failed:
1647
      _ThrowError("drbd%d: resize failed: %s", self.minor, result.output)
1648

    
1649

    
1650
class FileStorage(BlockDev):
1651
  """File device.
1652

1653
  This class represents the a file storage backend device.
1654

1655
  The unique_id for the file device is a (file_driver, file_path) tuple.
1656

1657
  """
1658
  def __init__(self, unique_id, children, size):
1659
    """Initalizes a file device backend.
1660

1661
    """
1662
    if children:
1663
      raise errors.BlockDeviceError("Invalid setup for file device")
1664
    super(FileStorage, self).__init__(unique_id, children, size)
1665
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1666
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1667
    self.driver = unique_id[0]
1668
    self.dev_path = unique_id[1]
1669
    self.Attach()
1670

    
1671
  def Assemble(self):
1672
    """Assemble the device.
1673

1674
    Checks whether the file device exists, raises BlockDeviceError otherwise.
1675

1676
    """
1677
    if not os.path.exists(self.dev_path):
1678
      _ThrowError("File device '%s' does not exist" % self.dev_path)
1679

    
1680
  def Shutdown(self):
1681
    """Shutdown the device.
1682

1683
    This is a no-op for the file type, as we don't deacivate
1684
    the file on shutdown.
1685

1686
    """
1687
    pass
1688

    
1689
  def Open(self, force=False):
1690
    """Make the device ready for I/O.
1691

1692
    This is a no-op for the file type.
1693

1694
    """
1695
    pass
1696

    
1697
  def Close(self):
1698
    """Notifies that the device will no longer be used for I/O.
1699

1700
    This is a no-op for the file type.
1701

1702
    """
1703
    pass
1704

    
1705
  def Remove(self):
1706
    """Remove the file backing the block device.
1707

1708
    @rtype: boolean
1709
    @return: True if the removal was successful
1710

1711
    """
1712
    try:
1713
      os.remove(self.dev_path)
1714
    except OSError, err:
1715
      if err.errno != errno.ENOENT:
1716
        _ThrowError("Can't remove file '%s': %s", self.dev_path, err)
1717

    
1718
  def Attach(self):
1719
    """Attach to an existing file.
1720

1721
    Check if this file already exists.
1722

1723
    @rtype: boolean
1724
    @return: True if file exists
1725

1726
    """
1727
    self.attached = os.path.exists(self.dev_path)
1728
    return self.attached
1729

    
1730
  @classmethod
1731
  def Create(cls, unique_id, children, size):
1732
    """Create a new file.
1733

1734
    @param size: the size of file in MiB
1735

1736
    @rtype: L{bdev.FileStorage}
1737
    @return: an instance of FileStorage
1738

1739
    """
1740
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1741
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1742
    dev_path = unique_id[1]
1743
    if os.path.exists(dev_path):
1744
      _ThrowError("File already existing: %s", dev_path)
1745
    try:
1746
      f = open(dev_path, 'w')
1747
      f.truncate(size * 1024 * 1024)
1748
      f.close()
1749
    except IOError, err:
1750
      _ThrowError("Error in file creation: %", str(err))
1751

    
1752
    return FileStorage(unique_id, children, size)
1753

    
1754

    
1755
DEV_MAP = {
1756
  constants.LD_LV: LogicalVolume,
1757
  constants.LD_DRBD8: DRBD8,
1758
  constants.LD_FILE: FileStorage,
1759
  }
1760

    
1761

    
1762
def FindDevice(dev_type, unique_id, children, size):
1763
  """Search for an existing, assembled device.
1764

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

1768
  """
1769
  if dev_type not in DEV_MAP:
1770
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1771
  device = DEV_MAP[dev_type](unique_id, children, size)
1772
  if not device.attached:
1773
    return None
1774
  return device
1775

    
1776

    
1777
def Assemble(dev_type, unique_id, children, size):
1778
  """Try to attach or assemble an existing device.
1779

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

1783
  """
1784
  if dev_type not in DEV_MAP:
1785
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1786
  device = DEV_MAP[dev_type](unique_id, children, size)
1787
  device.Assemble()
1788
  return device
1789

    
1790

    
1791
def Create(dev_type, unique_id, children, size):
1792
  """Create a device.
1793

1794
  """
1795
  if dev_type not in DEV_MAP:
1796
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1797
  device = DEV_MAP[dev_type].Create(unique_id, children, size)
1798
  return device