Statistics
| Branch: | Tag: | Revision:

root / lib / bdev.py @ 1dc10972

History | View | Annotate | Download (55.6 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 = 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
                           "-olv_attr,lv_kernel_major,lv_kernel_minor",
415
                           self.dev_path])
416
    if result.failed:
417
      logging.error("Can't find LV %s: %s, %s",
418
                    self.dev_path, result.fail_reason, result.output)
419
      return False
420
    out = result.stdout.strip().rstrip(',')
421
    out = out.split(",")
422
    if len(out) != 3:
423
      logging.error("Can't parse LVS output, len(%s) != 3", str(out))
424
      return False
425

    
426
    status, major, minor = out[:3]
427
    if len(status) != 6:
428
      logging.error("lvs lv_attr is not 6 characters (%s)", status)
429
      return False
430

    
431
    try:
432
      major = int(major)
433
      minor = int(minor)
434
    except ValueError, err:
435
      logging.error("lvs major/minor cannot be parsed: %s", str(err))
436

    
437
    self.major = major
438
    self.minor = minor
439
    self._degraded = status[0] == 'v' # virtual volume, i.e. doesn't backing
440
                                      # storage
441
    self.attached = True
442
    return True
443

    
444
  def Assemble(self):
445
    """Assemble the device.
446

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

451
    """
452
    result = utils.RunCmd(["lvchange", "-ay", self.dev_path])
453
    if result.failed:
454
      _ThrowError("Can't activate lv %s: %s", self.dev_path, result.output)
455

    
456
  def Shutdown(self):
457
    """Shutdown the device.
458

459
    This is a no-op for the LV device type, as we don't deactivate the
460
    volumes on shutdown.
461

462
    """
463
    pass
464

    
465
  def GetSyncStatus(self):
466
    """Returns the sync status of the device.
467

468
    If this device is a mirroring device, this function returns the
469
    status of the mirror.
470

471
    For logical volumes, sync_percent and estimated_time are always
472
    None (no recovery in progress, as we don't handle the mirrored LV
473
    case). The is_degraded parameter is the inverse of the ldisk
474
    parameter.
475

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

482
    The status was already read in Attach, so we just return it.
483

484
    @rtype: tuple
485
    @return: (sync_percent, estimated_time, is_degraded, ldisk)
486

487
    """
488
    return None, None, self._degraded, self._degraded
489

    
490
  def Open(self, force=False):
491
    """Make the device ready for I/O.
492

493
    This is a no-op for the LV device type.
494

495
    """
496
    pass
497

    
498
  def Close(self):
499
    """Notifies that the device will no longer be used for I/O.
500

501
    This is a no-op for the LV device type.
502

503
    """
504
    pass
505

    
506
  def Snapshot(self, size):
507
    """Create a snapshot copy of an lvm block device.
508

509
    """
510
    snap_name = self._lv_name + ".snap"
511

    
512
    # remove existing snapshot if found
513
    snap = LogicalVolume((self._vg_name, snap_name), None, size)
514
    _IgnoreError(snap.Remove)
515

    
516
    pvs_info = self.GetPVInfo(self._vg_name)
517
    if not pvs_info:
518
      _ThrowError("Can't compute PV info for vg %s", self._vg_name)
519
    pvs_info.sort()
520
    pvs_info.reverse()
521
    free_size, pv_name = pvs_info[0]
522
    if free_size < size:
523
      _ThrowError("Not enough free space: required %s,"
524
                  " available %s", size, free_size)
525

    
526
    result = utils.RunCmd(["lvcreate", "-L%dm" % size, "-s",
527
                           "-n%s" % snap_name, self.dev_path])
528
    if result.failed:
529
      _ThrowError("command: %s error: %s - %s",
530
                  result.cmd, result.fail_reason, result.output)
531

    
532
    return snap_name
533

    
534
  def SetInfo(self, text):
535
    """Update metadata with info text.
536

537
    """
538
    BlockDev.SetInfo(self, text)
539

    
540
    # Replace invalid characters
541
    text = re.sub('^[^A-Za-z0-9_+.]', '_', text)
542
    text = re.sub('[^-A-Za-z0-9_+.]', '_', text)
543

    
544
    # Only up to 128 characters are allowed
545
    text = text[:128]
546

    
547
    result = utils.RunCmd(["lvchange", "--addtag", text,
548
                           self.dev_path])
549
    if result.failed:
550
      _ThrowError("Command: %s error: %s - %s", result.cmd, result.fail_reason,
551
                  result.output)
552

    
553
  def Grow(self, amount):
554
    """Grow the logical volume.
555

556
    """
557
    # we try multiple algorithms since the 'best' ones might not have
558
    # space available in the right place, but later ones might (since
559
    # they have less constraints); also note that only recent LVM
560
    # supports 'cling'
561
    for alloc_policy in "contiguous", "cling", "normal":
562
      result = utils.RunCmd(["lvextend", "--alloc", alloc_policy,
563
                             "-L", "+%dm" % amount, self.dev_path])
564
      if not result.failed:
565
        return
566
    _ThrowError("Can't grow LV %s: %s", self.dev_path, result.output)
567

    
568

    
569
class DRBD8Status(object):
570
  """A DRBD status representation class.
571

572
  Note that this doesn't support unconfigured devices (cs:Unconfigured).
573

574
  """
575
  UNCONF_RE = re.compile(r"\s*[0-9]+:\s*cs:Unconfigured$")
576
  LINE_RE = re.compile(r"\s*[0-9]+:\s*cs:(\S+)\s+(?:st|ro):([^/]+)/(\S+)"
577
                       "\s+ds:([^/]+)/(\S+)\s+.*$")
578
  SYNC_RE = re.compile(r"^.*\ssync'ed:\s*([0-9.]+)%.*"
579
                       "\sfinish: ([0-9]+):([0-9]+):([0-9]+)\s.*$")
580

    
581
  CS_UNCONFIGURED = "Unconfigured"
582
  CS_STANDALONE = "StandAlone"
583
  CS_WFCONNECTION = "WFConnection"
584
  CS_WFREPORTPARAMS = "WFReportParams"
585
  CS_CONNECTED = "Connected"
586
  CS_STARTINGSYNCS = "StartingSyncS"
587
  CS_STARTINGSYNCT = "StartingSyncT"
588
  CS_WFBITMAPS = "WFBitMapS"
589
  CS_WFBITMAPT = "WFBitMapT"
590
  CS_WFSYNCUUID = "WFSyncUUID"
591
  CS_SYNCSOURCE = "SyncSource"
592
  CS_SYNCTARGET = "SyncTarget"
593
  CS_PAUSEDSYNCS = "PausedSyncS"
594
  CS_PAUSEDSYNCT = "PausedSyncT"
595
  CSET_SYNC = frozenset([
596
    CS_WFREPORTPARAMS,
597
    CS_STARTINGSYNCS,
598
    CS_STARTINGSYNCT,
599
    CS_WFBITMAPS,
600
    CS_WFBITMAPT,
601
    CS_WFSYNCUUID,
602
    CS_SYNCSOURCE,
603
    CS_SYNCTARGET,
604
    CS_PAUSEDSYNCS,
605
    CS_PAUSEDSYNCT,
606
    ])
607

    
608
  DS_DISKLESS = "Diskless"
609
  DS_ATTACHING = "Attaching" # transient state
610
  DS_FAILED = "Failed" # transient state, next: diskless
611
  DS_NEGOTIATING = "Negotiating" # transient state
612
  DS_INCONSISTENT = "Inconsistent" # while syncing or after creation
613
  DS_OUTDATED = "Outdated"
614
  DS_DUNKNOWN = "DUnknown" # shown for peer disk when not connected
615
  DS_CONSISTENT = "Consistent"
616
  DS_UPTODATE = "UpToDate" # normal state
617

    
618
  RO_PRIMARY = "Primary"
619
  RO_SECONDARY = "Secondary"
620
  RO_UNKNOWN = "Unknown"
621

    
622
  def __init__(self, procline):
623
    u = self.UNCONF_RE.match(procline)
624
    if u:
625
      self.cstatus = self.CS_UNCONFIGURED
626
      self.lrole = self.rrole = self.ldisk = self.rdisk = None
627
    else:
628
      m = self.LINE_RE.match(procline)
629
      if not m:
630
        raise errors.BlockDeviceError("Can't parse input data '%s'" % procline)
631
      self.cstatus = m.group(1)
632
      self.lrole = m.group(2)
633
      self.rrole = m.group(3)
634
      self.ldisk = m.group(4)
635
      self.rdisk = m.group(5)
636

    
637
    # end reading of data from the LINE_RE or UNCONF_RE
638

    
639
    self.is_standalone = self.cstatus == self.CS_STANDALONE
640
    self.is_wfconn = self.cstatus == self.CS_WFCONNECTION
641
    self.is_connected = self.cstatus == self.CS_CONNECTED
642
    self.is_primary = self.lrole == self.RO_PRIMARY
643
    self.is_secondary = self.lrole == self.RO_SECONDARY
644
    self.peer_primary = self.rrole == self.RO_PRIMARY
645
    self.peer_secondary = self.rrole == self.RO_SECONDARY
646
    self.both_primary = self.is_primary and self.peer_primary
647
    self.both_secondary = self.is_secondary and self.peer_secondary
648

    
649
    self.is_diskless = self.ldisk == self.DS_DISKLESS
650
    self.is_disk_uptodate = self.ldisk == self.DS_UPTODATE
651

    
652
    self.is_in_resync = self.cstatus in self.CSET_SYNC
653
    self.is_in_use = self.cstatus != self.CS_UNCONFIGURED
654

    
655
    m = self.SYNC_RE.match(procline)
656
    if m:
657
      self.sync_percent = float(m.group(1))
658
      hours = int(m.group(2))
659
      minutes = int(m.group(3))
660
      seconds = int(m.group(4))
661
      self.est_time = hours * 3600 + minutes * 60 + seconds
662
    else:
663
      # we have (in this if branch) no percent information, but if
664
      # we're resyncing we need to 'fake' a sync percent information,
665
      # as this is how cmdlib determines if it makes sense to wait for
666
      # resyncing or not
667
      if self.is_in_resync:
668
        self.sync_percent = 0
669
      else:
670
        self.sync_percent = None
671
      self.est_time = None
672

    
673

    
674
class BaseDRBD(BlockDev):
675
  """Base DRBD class.
676

677
  This class contains a few bits of common functionality between the
678
  0.7 and 8.x versions of DRBD.
679

680
  """
681
  _VERSION_RE = re.compile(r"^version: (\d+)\.(\d+)\.(\d+)"
682
                           r" \(api:(\d+)/proto:(\d+)(?:-(\d+))?\)")
683

    
684
  _DRBD_MAJOR = 147
685
  _ST_UNCONFIGURED = "Unconfigured"
686
  _ST_WFCONNECTION = "WFConnection"
687
  _ST_CONNECTED = "Connected"
688

    
689
  _STATUS_FILE = "/proc/drbd"
690

    
691
  @staticmethod
692
  def _GetProcData(filename=_STATUS_FILE):
693
    """Return data from /proc/drbd.
694

695
    """
696
    try:
697
      stat = open(filename, "r")
698
      try:
699
        data = stat.read().splitlines()
700
      finally:
701
        stat.close()
702
    except EnvironmentError, err:
703
      if err.errno == errno.ENOENT:
704
        _ThrowError("The file %s cannot be opened, check if the module"
705
                    " is loaded (%s)", filename, str(err))
706
      else:
707
        _ThrowError("Can't read the DRBD proc file %s: %s", filename, str(err))
708
    if not data:
709
      _ThrowError("Can't read any data from %s", filename)
710
    return data
711

    
712
  @staticmethod
713
  def _MassageProcData(data):
714
    """Transform the output of _GetProdData into a nicer form.
715

716
    @return: a dictionary of minor: joined lines from /proc/drbd
717
        for that minor
718

719
    """
720
    lmatch = re.compile("^ *([0-9]+):.*$")
721
    results = {}
722
    old_minor = old_line = None
723
    for line in data:
724
      lresult = lmatch.match(line)
725
      if lresult is not None:
726
        if old_minor is not None:
727
          results[old_minor] = old_line
728
        old_minor = int(lresult.group(1))
729
        old_line = line
730
      else:
731
        if old_minor is not None:
732
          old_line += " " + line.strip()
733
    # add last line
734
    if old_minor is not None:
735
      results[old_minor] = old_line
736
    return results
737

    
738
  @classmethod
739
  def _GetVersion(cls):
740
    """Return the DRBD version.
741

742
    This will return a dict with keys:
743
      - k_major
744
      - k_minor
745
      - k_point
746
      - api
747
      - proto
748
      - proto2 (only on drbd > 8.2.X)
749

750
    """
751
    proc_data = cls._GetProcData()
752
    first_line = proc_data[0].strip()
753
    version = cls._VERSION_RE.match(first_line)
754
    if not version:
755
      raise errors.BlockDeviceError("Can't parse DRBD version from '%s'" %
756
                                    first_line)
757

    
758
    values = version.groups()
759
    retval = {'k_major': int(values[0]),
760
              'k_minor': int(values[1]),
761
              'k_point': int(values[2]),
762
              'api': int(values[3]),
763
              'proto': int(values[4]),
764
             }
765
    if values[5] is not None:
766
      retval['proto2'] = values[5]
767

    
768
    return retval
769

    
770
  @staticmethod
771
  def _DevPath(minor):
772
    """Return the path to a drbd device for a given minor.
773

774
    """
775
    return "/dev/drbd%d" % minor
776

    
777
  @classmethod
778
  def GetUsedDevs(cls):
779
    """Compute the list of used DRBD devices.
780

781
    """
782
    data = cls._GetProcData()
783

    
784
    used_devs = {}
785
    valid_line = re.compile("^ *([0-9]+): cs:([^ ]+).*$")
786
    for line in data:
787
      match = valid_line.match(line)
788
      if not match:
789
        continue
790
      minor = int(match.group(1))
791
      state = match.group(2)
792
      if state == cls._ST_UNCONFIGURED:
793
        continue
794
      used_devs[minor] = state, line
795

    
796
    return used_devs
797

    
798
  def _SetFromMinor(self, minor):
799
    """Set our parameters based on the given minor.
800

801
    This sets our minor variable and our dev_path.
802

803
    """
804
    if minor is None:
805
      self.minor = self.dev_path = None
806
      self.attached = False
807
    else:
808
      self.minor = minor
809
      self.dev_path = self._DevPath(minor)
810
      self.attached = True
811

    
812
  @staticmethod
813
  def _CheckMetaSize(meta_device):
814
    """Check if the given meta device looks like a valid one.
815

816
    This currently only check the size, which must be around
817
    128MiB.
818

819
    """
820
    result = utils.RunCmd(["blockdev", "--getsize", meta_device])
821
    if result.failed:
822
      _ThrowError("Failed to get device size: %s - %s",
823
                  result.fail_reason, result.output)
824
    try:
825
      sectors = int(result.stdout)
826
    except ValueError:
827
      _ThrowError("Invalid output from blockdev: '%s'", result.stdout)
828
    bytes = sectors * 512
829
    if bytes < 128 * 1024 * 1024: # less than 128MiB
830
      _ThrowError("Meta device too small (%.2fMib)", (bytes / 1024 / 1024))
831
    # the maximum *valid* size of the meta device when living on top
832
    # of LVM is hard to compute: it depends on the number of stripes
833
    # and the PE size; e.g. a 2-stripe, 64MB PE will result in a 128MB
834
    # (normal size), but an eight-stripe 128MB PE will result in a 1GB
835
    # size meta device; as such, we restrict it to 1GB (a little bit
836
    # too generous, but making assumptions about PE size is hard)
837
    if bytes > 1024 * 1024 * 1024:
838
      _ThrowError("Meta device too big (%.2fMiB)", (bytes / 1024 / 1024))
839

    
840
  def Rename(self, new_id):
841
    """Rename a device.
842

843
    This is not supported for drbd devices.
844

845
    """
846
    raise errors.ProgrammerError("Can't rename a drbd device")
847

    
848

    
849
class DRBD8(BaseDRBD):
850
  """DRBD v8.x block device.
851

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

856
  The unique_id for the drbd device is the (local_ip, local_port,
857
  remote_ip, remote_port) tuple, and it must have two children: the
858
  data device and the meta_device. The meta device is checked for
859
  valid size and is zeroed on create.
860

861
  """
862
  _MAX_MINORS = 255
863
  _PARSE_SHOW = None
864

    
865
  # timeout constants
866
  _NET_RECONFIG_TIMEOUT = 60
867

    
868
  def __init__(self, unique_id, children, size):
869
    if children and children.count(None) > 0:
870
      children = []
871
    super(DRBD8, self).__init__(unique_id, children, size)
872
    self.major = self._DRBD_MAJOR
873
    version = self._GetVersion()
874
    if version['k_major'] != 8 :
875
      _ThrowError("Mismatch in DRBD kernel version and requested ganeti"
876
                  " usage: kernel is %s.%s, ganeti wants 8.x",
877
                  version['k_major'], version['k_minor'])
878

    
879
    if len(children) not in (0, 2):
880
      raise ValueError("Invalid configuration data %s" % str(children))
881
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 6:
882
      raise ValueError("Invalid configuration data %s" % str(unique_id))
883
    (self._lhost, self._lport,
884
     self._rhost, self._rport,
885
     self._aminor, self._secret) = unique_id
886
    if (self._lhost is not None and self._lhost == self._rhost and
887
        self._lport == self._rport):
888
      raise ValueError("Invalid configuration data, same local/remote %s" %
889
                       (unique_id,))
890
    self.Attach()
891

    
892
  @classmethod
893
  def _InitMeta(cls, minor, dev_path):
894
    """Initialize a meta device.
895

896
    This will not work if the given minor is in use.
897

898
    """
899
    result = utils.RunCmd(["drbdmeta", "--force", cls._DevPath(minor),
900
                           "v08", dev_path, "0", "create-md"])
901
    if result.failed:
902
      _ThrowError("Can't initialize meta device: %s", result.output)
903

    
904
  @classmethod
905
  def _FindUnusedMinor(cls):
906
    """Find an unused DRBD device.
907

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

911
    """
912
    data = cls._GetProcData()
913

    
914
    unused_line = re.compile("^ *([0-9]+): cs:Unconfigured$")
915
    used_line = re.compile("^ *([0-9]+): cs:")
916
    highest = None
917
    for line in data:
918
      match = unused_line.match(line)
919
      if match:
920
        return int(match.group(1))
921
      match = used_line.match(line)
922
      if match:
923
        minor = int(match.group(1))
924
        highest = max(highest, minor)
925
    if highest is None: # there are no minors in use at all
926
      return 0
927
    if highest >= cls._MAX_MINORS:
928
      logging.error("Error: no free drbd minors!")
929
      raise errors.BlockDeviceError("Can't find a free DRBD minor")
930
    return highest + 1
931

    
932
  @classmethod
933
  def _GetShowParser(cls):
934
    """Return a parser for `drbd show` output.
935

936
    This will either create or return an already-create parser for the
937
    output of the command `drbd show`.
938

939
    """
940
    if cls._PARSE_SHOW is not None:
941
      return cls._PARSE_SHOW
942

    
943
    # pyparsing setup
944
    lbrace = pyp.Literal("{").suppress()
945
    rbrace = pyp.Literal("}").suppress()
946
    semi = pyp.Literal(";").suppress()
947
    # this also converts the value to an int
948
    number = pyp.Word(pyp.nums).setParseAction(lambda s, l, t: int(t[0]))
949

    
950
    comment = pyp.Literal ("#") + pyp.Optional(pyp.restOfLine)
951
    defa = pyp.Literal("_is_default").suppress()
952
    dbl_quote = pyp.Literal('"').suppress()
953

    
954
    keyword = pyp.Word(pyp.alphanums + '-')
955

    
956
    # value types
957
    value = pyp.Word(pyp.alphanums + '_-/.:')
958
    quoted = dbl_quote + pyp.CharsNotIn('"') + dbl_quote
959
    addr_type = (pyp.Optional(pyp.Literal("ipv4")).suppress() +
960
                 pyp.Optional(pyp.Literal("ipv6")).suppress())
961
    addr_port = (addr_type + pyp.Word(pyp.nums + '.') +
962
                 pyp.Literal(':').suppress() + number)
963
    # meta device, extended syntax
964
    meta_value = ((value ^ quoted) + pyp.Literal('[').suppress() +
965
                  number + pyp.Word(']').suppress())
966
    # device name, extended syntax
967
    device_value = pyp.Literal("minor").suppress() + number
968

    
969
    # a statement
970
    stmt = (~rbrace + keyword + ~lbrace +
971
            pyp.Optional(addr_port ^ value ^ quoted ^ meta_value ^
972
                         device_value) +
973
            pyp.Optional(defa) + semi +
974
            pyp.Optional(pyp.restOfLine).suppress())
975

    
976
    # an entire section
977
    section_name = pyp.Word(pyp.alphas + '_')
978
    section = section_name + lbrace + pyp.ZeroOrMore(pyp.Group(stmt)) + rbrace
979

    
980
    bnf = pyp.ZeroOrMore(pyp.Group(section ^ stmt))
981
    bnf.ignore(comment)
982

    
983
    cls._PARSE_SHOW = bnf
984

    
985
    return bnf
986

    
987
  @classmethod
988
  def _GetShowData(cls, minor):
989
    """Return the `drbdsetup show` data for a minor.
990

991
    """
992
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "show"])
993
    if result.failed:
994
      logging.error("Can't display the drbd config: %s - %s",
995
                    result.fail_reason, result.output)
996
      return None
997
    return result.stdout
998

    
999
  @classmethod
1000
  def _GetDevInfo(cls, out):
1001
    """Parse details about a given DRBD minor.
1002

1003
    This return, if available, the local backing device (as a path)
1004
    and the local and remote (ip, port) information from a string
1005
    containing the output of the `drbdsetup show` command as returned
1006
    by _GetShowData.
1007

1008
    """
1009
    data = {}
1010
    if not out:
1011
      return data
1012

    
1013
    bnf = cls._GetShowParser()
1014
    # run pyparse
1015

    
1016
    try:
1017
      results = bnf.parseString(out)
1018
    except pyp.ParseException, err:
1019
      _ThrowError("Can't parse drbdsetup show output: %s", str(err))
1020

    
1021
    # and massage the results into our desired format
1022
    for section in results:
1023
      sname = section[0]
1024
      if sname == "_this_host":
1025
        for lst in section[1:]:
1026
          if lst[0] == "disk":
1027
            data["local_dev"] = lst[1]
1028
          elif lst[0] == "meta-disk":
1029
            data["meta_dev"] = lst[1]
1030
            data["meta_index"] = lst[2]
1031
          elif lst[0] == "address":
1032
            data["local_addr"] = tuple(lst[1:])
1033
      elif sname == "_remote_host":
1034
        for lst in section[1:]:
1035
          if lst[0] == "address":
1036
            data["remote_addr"] = tuple(lst[1:])
1037
    return data
1038

    
1039
  def _MatchesLocal(self, info):
1040
    """Test if our local config matches with an existing device.
1041

1042
    The parameter should be as returned from `_GetDevInfo()`. This
1043
    method tests if our local backing device is the same as the one in
1044
    the info parameter, in effect testing if we look like the given
1045
    device.
1046

1047
    """
1048
    if self._children:
1049
      backend, meta = self._children
1050
    else:
1051
      backend = meta = None
1052

    
1053
    if backend is not None:
1054
      retval = ("local_dev" in info and info["local_dev"] == backend.dev_path)
1055
    else:
1056
      retval = ("local_dev" not in info)
1057

    
1058
    if meta is not None:
1059
      retval = retval and ("meta_dev" in info and
1060
                           info["meta_dev"] == meta.dev_path)
1061
      retval = retval and ("meta_index" in info and
1062
                           info["meta_index"] == 0)
1063
    else:
1064
      retval = retval and ("meta_dev" not in info and
1065
                           "meta_index" not in info)
1066
    return retval
1067

    
1068
  def _MatchesNet(self, info):
1069
    """Test if our network config matches with an existing device.
1070

1071
    The parameter should be as returned from `_GetDevInfo()`. This
1072
    method tests if our network configuration is the same as the one
1073
    in the info parameter, in effect testing if we look like the given
1074
    device.
1075

1076
    """
1077
    if (((self._lhost is None and not ("local_addr" in info)) and
1078
         (self._rhost is None and not ("remote_addr" in info)))):
1079
      return True
1080

    
1081
    if self._lhost is None:
1082
      return False
1083

    
1084
    if not ("local_addr" in info and
1085
            "remote_addr" in info):
1086
      return False
1087

    
1088
    retval = (info["local_addr"] == (self._lhost, self._lport))
1089
    retval = (retval and
1090
              info["remote_addr"] == (self._rhost, self._rport))
1091
    return retval
1092

    
1093
  @classmethod
1094
  def _AssembleLocal(cls, minor, backend, meta, size):
1095
    """Configure the local part of a DRBD device.
1096

1097
    """
1098
    args = ["drbdsetup", cls._DevPath(minor), "disk",
1099
            backend, meta, "0",
1100
            "-d", "%sm" % size,
1101
            "-e", "detach",
1102
            "--create-device"]
1103
    result = utils.RunCmd(args)
1104
    if result.failed:
1105
      _ThrowError("drbd%d: can't attach local disk: %s", minor, result.output)
1106

    
1107
  @classmethod
1108
  def _AssembleNet(cls, minor, net_info, protocol,
1109
                   dual_pri=False, hmac=None, secret=None):
1110
    """Configure the network part of the device.
1111

1112
    """
1113
    lhost, lport, rhost, rport = net_info
1114
    if None in net_info:
1115
      # we don't want network connection and actually want to make
1116
      # sure its shutdown
1117
      cls._ShutdownNet(minor)
1118
      return
1119

    
1120
    # Workaround for a race condition. When DRBD is doing its dance to
1121
    # establish a connection with its peer, it also sends the
1122
    # synchronization speed over the wire. In some cases setting the
1123
    # sync speed only after setting up both sides can race with DRBD
1124
    # connecting, hence we set it here before telling DRBD anything
1125
    # about its peer.
1126
    cls._SetMinorSyncSpeed(minor, constants.SYNC_SPEED)
1127

    
1128
    args = ["drbdsetup", cls._DevPath(minor), "net",
1129
            "%s:%s" % (lhost, lport), "%s:%s" % (rhost, rport), protocol,
1130
            "-A", "discard-zero-changes",
1131
            "-B", "consensus",
1132
            "--create-device",
1133
            ]
1134
    if dual_pri:
1135
      args.append("-m")
1136
    if hmac and secret:
1137
      args.extend(["-a", hmac, "-x", secret])
1138
    result = utils.RunCmd(args)
1139
    if result.failed:
1140
      _ThrowError("drbd%d: can't setup network: %s - %s",
1141
                  minor, result.fail_reason, result.output)
1142

    
1143
    timeout = time.time() + 10
1144
    ok = False
1145
    while time.time() < timeout:
1146
      info = cls._GetDevInfo(cls._GetShowData(minor))
1147
      if not "local_addr" in info or not "remote_addr" in info:
1148
        time.sleep(1)
1149
        continue
1150
      if (info["local_addr"] != (lhost, lport) or
1151
          info["remote_addr"] != (rhost, rport)):
1152
        time.sleep(1)
1153
        continue
1154
      ok = True
1155
      break
1156
    if not ok:
1157
      _ThrowError("drbd%d: timeout while configuring network", minor)
1158

    
1159
  def AddChildren(self, devices):
1160
    """Add a disk to the DRBD device.
1161

1162
    """
1163
    if self.minor is None:
1164
      _ThrowError("drbd%d: can't attach to dbrd8 during AddChildren",
1165
                  self._aminor)
1166
    if len(devices) != 2:
1167
      _ThrowError("drbd%d: need two devices for AddChildren", self.minor)
1168
    info = self._GetDevInfo(self._GetShowData(self.minor))
1169
    if "local_dev" in info:
1170
      _ThrowError("drbd%d: already attached to a local disk", self.minor)
1171
    backend, meta = devices
1172
    if backend.dev_path is None or meta.dev_path is None:
1173
      _ThrowError("drbd%d: children not ready during AddChildren", self.minor)
1174
    backend.Open()
1175
    meta.Open()
1176
    self._CheckMetaSize(meta.dev_path)
1177
    self._InitMeta(self._FindUnusedMinor(), meta.dev_path)
1178

    
1179
    self._AssembleLocal(self.minor, backend.dev_path, meta.dev_path, self.size)
1180
    self._children = devices
1181

    
1182
  def RemoveChildren(self, devices):
1183
    """Detach the drbd device from local storage.
1184

1185
    """
1186
    if self.minor is None:
1187
      _ThrowError("drbd%d: can't attach to drbd8 during RemoveChildren",
1188
                  self._aminor)
1189
    # early return if we don't actually have backing storage
1190
    info = self._GetDevInfo(self._GetShowData(self.minor))
1191
    if "local_dev" not in info:
1192
      return
1193
    if len(self._children) != 2:
1194
      _ThrowError("drbd%d: we don't have two children: %s", self.minor,
1195
                  self._children)
1196
    if self._children.count(None) == 2: # we don't actually have children :)
1197
      logging.warning("drbd%d: requested detach while detached", self.minor)
1198
      return
1199
    if len(devices) != 2:
1200
      _ThrowError("drbd%d: we need two children in RemoveChildren", self.minor)
1201
    for child, dev in zip(self._children, devices):
1202
      if dev != child.dev_path:
1203
        _ThrowError("drbd%d: mismatch in local storage (%s != %s) in"
1204
                    " RemoveChildren", self.minor, dev, child.dev_path)
1205

    
1206
    self._ShutdownLocal(self.minor)
1207
    self._children = []
1208

    
1209
  @classmethod
1210
  def _SetMinorSyncSpeed(cls, minor, kbytes):
1211
    """Set the speed of the DRBD syncer.
1212

1213
    This is the low-level implementation.
1214

1215
    @type minor: int
1216
    @param minor: the drbd minor whose settings we change
1217
    @type kbytes: int
1218
    @param kbytes: the speed in kbytes/second
1219
    @rtype: boolean
1220
    @return: the success of the operation
1221

1222
    """
1223
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "syncer",
1224
                           "-r", "%d" % kbytes, "--create-device"])
1225
    if result.failed:
1226
      logging.error("Can't change syncer rate: %s - %s",
1227
                    result.fail_reason, result.output)
1228
    return not result.failed
1229

    
1230
  def SetSyncSpeed(self, kbytes):
1231
    """Set the speed of the DRBD syncer.
1232

1233
    @type kbytes: int
1234
    @param kbytes: the speed in kbytes/second
1235
    @rtype: boolean
1236
    @return: the success of the operation
1237

1238
    """
1239
    if self.minor is None:
1240
      logging.info("Not attached during SetSyncSpeed")
1241
      return False
1242
    children_result = super(DRBD8, self).SetSyncSpeed(kbytes)
1243
    return self._SetMinorSyncSpeed(self.minor, kbytes) and children_result
1244

    
1245
  def GetProcStatus(self):
1246
    """Return device data from /proc.
1247

1248
    """
1249
    if self.minor is None:
1250
      _ThrowError("drbd%d: GetStats() called while not attached", self._aminor)
1251
    proc_info = self._MassageProcData(self._GetProcData())
1252
    if self.minor not in proc_info:
1253
      _ThrowError("drbd%d: can't find myself in /proc", self.minor)
1254
    return DRBD8Status(proc_info[self.minor])
1255

    
1256
  def GetSyncStatus(self):
1257
    """Returns the sync status of the device.
1258

1259

1260
    If sync_percent is None, it means all is ok
1261
    If estimated_time is None, it means we can't esimate
1262
    the time needed, otherwise it's the time left in seconds.
1263

1264

1265
    We set the is_degraded parameter to True on two conditions:
1266
    network not connected or local disk missing.
1267

1268
    We compute the ldisk parameter based on wheter we have a local
1269
    disk or not.
1270

1271
    @rtype: tuple
1272
    @return: (sync_percent, estimated_time, is_degraded, ldisk)
1273

1274
    """
1275
    if self.minor is None and not self.Attach():
1276
      _ThrowError("drbd%d: can't Attach() in GetSyncStatus", self._aminor)
1277
    stats = self.GetProcStatus()
1278
    ldisk = not stats.is_disk_uptodate
1279
    is_degraded = not stats.is_connected
1280
    return stats.sync_percent, stats.est_time, is_degraded or ldisk, ldisk
1281

    
1282
  def Open(self, force=False):
1283
    """Make the local state primary.
1284

1285
    If the 'force' parameter is given, the '-o' option is passed to
1286
    drbdsetup. Since this is a potentially dangerous operation, the
1287
    force flag should be only given after creation, when it actually
1288
    is mandatory.
1289

1290
    """
1291
    if self.minor is None and not self.Attach():
1292
      logging.error("DRBD cannot attach to a device during open")
1293
      return False
1294
    cmd = ["drbdsetup", self.dev_path, "primary"]
1295
    if force:
1296
      cmd.append("-o")
1297
    result = utils.RunCmd(cmd)
1298
    if result.failed:
1299
      _ThrowError("drbd%d: can't make drbd device primary: %s", self.minor,
1300
                  result.output)
1301

    
1302
  def Close(self):
1303
    """Make the local state secondary.
1304

1305
    This will, of course, fail if the device is in use.
1306

1307
    """
1308
    if self.minor is None and not self.Attach():
1309
      _ThrowError("drbd%d: can't Attach() in Close()", self._aminor)
1310
    result = utils.RunCmd(["drbdsetup", self.dev_path, "secondary"])
1311
    if result.failed:
1312
      _ThrowError("drbd%d: can't switch drbd device to secondary: %s",
1313
                  self.minor, result.output)
1314

    
1315
  def DisconnectNet(self):
1316
    """Removes network configuration.
1317

1318
    This method shutdowns the network side of the device.
1319

1320
    The method will wait up to a hardcoded timeout for the device to
1321
    go into standalone after the 'disconnect' command before
1322
    re-configuring it, as sometimes it takes a while for the
1323
    disconnect to actually propagate and thus we might issue a 'net'
1324
    command while the device is still connected. If the device will
1325
    still be attached to the network and we time out, we raise an
1326
    exception.
1327

1328
    """
1329
    if self.minor is None:
1330
      _ThrowError("drbd%d: disk not attached in re-attach net", self._aminor)
1331

    
1332
    if None in (self._lhost, self._lport, self._rhost, self._rport):
1333
      _ThrowError("drbd%d: DRBD disk missing network info in"
1334
                  " DisconnectNet()", self.minor)
1335

    
1336
    ever_disconnected = _IgnoreError(self._ShutdownNet, self.minor)
1337
    timeout_limit = time.time() + self._NET_RECONFIG_TIMEOUT
1338
    sleep_time = 0.100 # we start the retry time at 100 miliseconds
1339
    while time.time() < timeout_limit:
1340
      status = self.GetProcStatus()
1341
      if status.is_standalone:
1342
        break
1343
      # retry the disconnect, it seems possible that due to a
1344
      # well-time disconnect on the peer, my disconnect command might
1345
      # be ingored and forgotten
1346
      ever_disconnected = _IgnoreError(self._ShutdownNet, self.minor) or \
1347
                          ever_disconnected
1348
      time.sleep(sleep_time)
1349
      sleep_time = min(2, sleep_time * 1.5)
1350

    
1351
    if not status.is_standalone:
1352
      if ever_disconnected:
1353
        msg = ("drbd%d: device did not react to the"
1354
               " 'disconnect' command in a timely manner")
1355
      else:
1356
        msg = "drbd%d: can't shutdown network, even after multiple retries"
1357
      _ThrowError(msg, self.minor)
1358

    
1359
    reconfig_time = time.time() - timeout_limit + self._NET_RECONFIG_TIMEOUT
1360
    if reconfig_time > 15: # hardcoded alert limit
1361
      logging.info("drbd%d: DisconnectNet: detach took %.3f seconds",
1362
                   self.minor, reconfig_time)
1363

    
1364
  def AttachNet(self, multimaster):
1365
    """Reconnects the network.
1366

1367
    This method connects the network side of the device with a
1368
    specified multi-master flag. The device needs to be 'Standalone'
1369
    but have valid network configuration data.
1370

1371
    Args:
1372
      - multimaster: init the network in dual-primary mode
1373

1374
    """
1375
    if self.minor is None:
1376
      _ThrowError("drbd%d: device not attached in AttachNet", self._aminor)
1377

    
1378
    if None in (self._lhost, self._lport, self._rhost, self._rport):
1379
      _ThrowError("drbd%d: missing network info in AttachNet()", self.minor)
1380

    
1381
    status = self.GetProcStatus()
1382

    
1383
    if not status.is_standalone:
1384
      _ThrowError("drbd%d: device is not standalone in AttachNet", self.minor)
1385

    
1386
    self._AssembleNet(self.minor,
1387
                      (self._lhost, self._lport, self._rhost, self._rport),
1388
                      constants.DRBD_NET_PROTOCOL, dual_pri=multimaster,
1389
                      hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1390

    
1391
  def Attach(self):
1392
    """Check if our minor is configured.
1393

1394
    This doesn't do any device configurations - it only checks if the
1395
    minor is in a state different from Unconfigured.
1396

1397
    Note that this function will not change the state of the system in
1398
    any way (except in case of side-effects caused by reading from
1399
    /proc).
1400

1401
    """
1402
    used_devs = self.GetUsedDevs()
1403
    if self._aminor in used_devs:
1404
      minor = self._aminor
1405
    else:
1406
      minor = None
1407

    
1408
    self._SetFromMinor(minor)
1409
    return minor is not None
1410

    
1411
  def Assemble(self):
1412
    """Assemble the drbd.
1413

1414
    Method:
1415
      - if we have a configured device, we try to ensure that it matches
1416
        our config
1417
      - if not, we create it from zero
1418

1419
    """
1420
    super(DRBD8, self).Assemble()
1421

    
1422
    self.Attach()
1423
    if self.minor is None:
1424
      # local device completely unconfigured
1425
      self._FastAssemble()
1426
    else:
1427
      # we have to recheck the local and network status and try to fix
1428
      # the device
1429
      self._SlowAssemble()
1430

    
1431
  def _SlowAssemble(self):
1432
    """Assembles the DRBD device from a (partially) configured device.
1433

1434
    In case of partially attached (local device matches but no network
1435
    setup), we perform the network attach. If successful, we re-test
1436
    the attach if can return success.
1437

1438
    """
1439
    net_data = (self._lhost, self._lport, self._rhost, self._rport)
1440
    for minor in (self._aminor,):
1441
      info = self._GetDevInfo(self._GetShowData(minor))
1442
      match_l = self._MatchesLocal(info)
1443
      match_r = self._MatchesNet(info)
1444

    
1445
      if match_l and match_r:
1446
        # everything matches
1447
        break
1448

    
1449
      if match_l and not match_r and "local_addr" not in info:
1450
        # disk matches, but not attached to network, attach and recheck
1451
        self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
1452
                          hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1453
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1454
          break
1455
        else:
1456
          _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
1457
                      " show' disagrees", minor)
1458

    
1459
      if match_r and "local_dev" not in info:
1460
        # no local disk, but network attached and it matches
1461
        self._AssembleLocal(minor, self._children[0].dev_path,
1462
                            self._children[1].dev_path, self.size)
1463
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1464
          break
1465
        else:
1466
          _ThrowError("drbd%d: disk attach successful, but 'drbdsetup"
1467
                      " show' disagrees", minor)
1468

    
1469
      # this case must be considered only if we actually have local
1470
      # storage, i.e. not in diskless mode, because all diskless
1471
      # devices are equal from the point of view of local
1472
      # configuration
1473
      if (match_l and "local_dev" in info and
1474
          not match_r and "local_addr" in info):
1475
        # strange case - the device network part points to somewhere
1476
        # else, even though its local storage is ours; as we own the
1477
        # drbd space, we try to disconnect from the remote peer and
1478
        # reconnect to our correct one
1479
        try:
1480
          self._ShutdownNet(minor)
1481
        except errors.BlockDeviceError, err:
1482
          _ThrowError("drbd%d: device has correct local storage, wrong"
1483
                      " remote peer and is unable to disconnect in order"
1484
                      " to attach to the correct peer: %s", minor, str(err))
1485
        # note: _AssembleNet also handles the case when we don't want
1486
        # local storage (i.e. one or more of the _[lr](host|port) is
1487
        # None)
1488
        self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
1489
                          hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1490
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1491
          break
1492
        else:
1493
          _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
1494
                      " show' disagrees", minor)
1495

    
1496
    else:
1497
      minor = None
1498

    
1499
    self._SetFromMinor(minor)
1500
    if minor is None:
1501
      _ThrowError("drbd%d: cannot activate, unknown or unhandled reason",
1502
                  self._aminor)
1503

    
1504
  def _FastAssemble(self):
1505
    """Assemble the drbd device from zero.
1506

1507
    This is run when in Assemble we detect our minor is unused.
1508

1509
    """
1510
    minor = self._aminor
1511
    if self._children and self._children[0] and self._children[1]:
1512
      self._AssembleLocal(minor, self._children[0].dev_path,
1513
                          self._children[1].dev_path, self.size)
1514
    if self._lhost and self._lport and self._rhost and self._rport:
1515
      self._AssembleNet(minor,
1516
                        (self._lhost, self._lport, self._rhost, self._rport),
1517
                        constants.DRBD_NET_PROTOCOL,
1518
                        hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1519
    self._SetFromMinor(minor)
1520

    
1521
  @classmethod
1522
  def _ShutdownLocal(cls, minor):
1523
    """Detach from the local device.
1524

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

1528
    """
1529
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "detach"])
1530
    if result.failed:
1531
      _ThrowError("drbd%d: can't detach local disk: %s", minor, result.output)
1532

    
1533
  @classmethod
1534
  def _ShutdownNet(cls, minor):
1535
    """Disconnect from the remote peer.
1536

1537
    This fails if we don't have a local device.
1538

1539
    """
1540
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disconnect"])
1541
    if result.failed:
1542
      _ThrowError("drbd%d: can't shutdown network: %s", minor, result.output)
1543

    
1544
  @classmethod
1545
  def _ShutdownAll(cls, minor):
1546
    """Deactivate the device.
1547

1548
    This will, of course, fail if the device is in use.
1549

1550
    """
1551
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "down"])
1552
    if result.failed:
1553
      _ThrowError("drbd%d: can't shutdown drbd device: %s",
1554
                  minor, result.output)
1555

    
1556
  def Shutdown(self):
1557
    """Shutdown the DRBD device.
1558

1559
    """
1560
    if self.minor is None and not self.Attach():
1561
      logging.info("drbd%d: not attached during Shutdown()", self._aminor)
1562
      return
1563
    minor = self.minor
1564
    self.minor = None
1565
    self.dev_path = None
1566
    self._ShutdownAll(minor)
1567

    
1568
  def Remove(self):
1569
    """Stub remove for DRBD devices.
1570

1571
    """
1572
    self.Shutdown()
1573

    
1574
  @classmethod
1575
  def Create(cls, unique_id, children, size):
1576
    """Create a new DRBD8 device.
1577

1578
    Since DRBD devices are not created per se, just assembled, this
1579
    function only initializes the metadata.
1580

1581
    """
1582
    if len(children) != 2:
1583
      raise errors.ProgrammerError("Invalid setup for the drbd device")
1584
    # check that the minor is unused
1585
    aminor = unique_id[4]
1586
    proc_info = cls._MassageProcData(cls._GetProcData())
1587
    if aminor in proc_info:
1588
      status = DRBD8Status(proc_info[aminor])
1589
      in_use = status.is_in_use
1590
    else:
1591
      in_use = False
1592
    if in_use:
1593
      _ThrowError("drbd%d: minor is already in use at Create() time", aminor)
1594
    meta = children[1]
1595
    meta.Assemble()
1596
    if not meta.Attach():
1597
      _ThrowError("drbd%d: can't attach to meta device '%s'",
1598
                  aminor, meta)
1599
    cls._CheckMetaSize(meta.dev_path)
1600
    cls._InitMeta(aminor, meta.dev_path)
1601
    return cls(unique_id, children, size)
1602

    
1603
  def Grow(self, amount):
1604
    """Resize the DRBD device and its backing storage.
1605

1606
    """
1607
    if self.minor is None:
1608
      _ThrowError("drbd%d: Grow called while not attached", self._aminor)
1609
    if len(self._children) != 2 or None in self._children:
1610
      _ThrowError("drbd%d: cannot grow diskless device", self.minor)
1611
    self._children[0].Grow(amount)
1612
    result = utils.RunCmd(["drbdsetup", self.dev_path, "resize"])
1613
    if result.failed:
1614
      _ThrowError("drbd%d: resize failed: %s", self.minor, result.output)
1615

    
1616

    
1617
class FileStorage(BlockDev):
1618
  """File device.
1619

1620
  This class represents the a file storage backend device.
1621

1622
  The unique_id for the file device is a (file_driver, file_path) tuple.
1623

1624
  """
1625
  def __init__(self, unique_id, children, size):
1626
    """Initalizes a file device backend.
1627

1628
    """
1629
    if children:
1630
      raise errors.BlockDeviceError("Invalid setup for file device")
1631
    super(FileStorage, self).__init__(unique_id, children, size)
1632
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1633
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1634
    self.driver = unique_id[0]
1635
    self.dev_path = unique_id[1]
1636
    self.Attach()
1637

    
1638
  def Assemble(self):
1639
    """Assemble the device.
1640

1641
    Checks whether the file device exists, raises BlockDeviceError otherwise.
1642

1643
    """
1644
    if not os.path.exists(self.dev_path):
1645
      _ThrowError("File device '%s' does not exist" % self.dev_path)
1646

    
1647
  def Shutdown(self):
1648
    """Shutdown the device.
1649

1650
    This is a no-op for the file type, as we don't deacivate
1651
    the file on shutdown.
1652

1653
    """
1654
    pass
1655

    
1656
  def Open(self, force=False):
1657
    """Make the device ready for I/O.
1658

1659
    This is a no-op for the file type.
1660

1661
    """
1662
    pass
1663

    
1664
  def Close(self):
1665
    """Notifies that the device will no longer be used for I/O.
1666

1667
    This is a no-op for the file type.
1668

1669
    """
1670
    pass
1671

    
1672
  def Remove(self):
1673
    """Remove the file backing the block device.
1674

1675
    @rtype: boolean
1676
    @return: True if the removal was successful
1677

1678
    """
1679
    try:
1680
      os.remove(self.dev_path)
1681
    except OSError, err:
1682
      if err.errno != errno.ENOENT:
1683
        _ThrowError("Can't remove file '%s': %s", self.dev_path, err)
1684

    
1685
  def Attach(self):
1686
    """Attach to an existing file.
1687

1688
    Check if this file already exists.
1689

1690
    @rtype: boolean
1691
    @return: True if file exists
1692

1693
    """
1694
    self.attached = os.path.exists(self.dev_path)
1695
    return self.attached
1696

    
1697
  @classmethod
1698
  def Create(cls, unique_id, children, size):
1699
    """Create a new file.
1700

1701
    @param size: the size of file in MiB
1702

1703
    @rtype: L{bdev.FileStorage}
1704
    @return: an instance of FileStorage
1705

1706
    """
1707
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1708
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1709
    dev_path = unique_id[1]
1710
    if os.path.exists(dev_path):
1711
      _ThrowError("File already existing: %s", dev_path)
1712
    try:
1713
      f = open(dev_path, 'w')
1714
      f.truncate(size * 1024 * 1024)
1715
      f.close()
1716
    except IOError, err:
1717
      _ThrowError("Error in file creation: %", str(err))
1718

    
1719
    return FileStorage(unique_id, children, size)
1720

    
1721

    
1722
DEV_MAP = {
1723
  constants.LD_LV: LogicalVolume,
1724
  constants.LD_DRBD8: DRBD8,
1725
  constants.LD_FILE: FileStorage,
1726
  }
1727

    
1728

    
1729
def FindDevice(dev_type, unique_id, children, size):
1730
  """Search for an existing, assembled device.
1731

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

1735
  """
1736
  if dev_type not in DEV_MAP:
1737
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1738
  device = DEV_MAP[dev_type](unique_id, children, size)
1739
  if not device.attached:
1740
    return None
1741
  return device
1742

    
1743

    
1744
def Assemble(dev_type, unique_id, children, size):
1745
  """Try to attach or assemble an existing device.
1746

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

1750
  """
1751
  if dev_type not in DEV_MAP:
1752
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1753
  device = DEV_MAP[dev_type](unique_id, children, size)
1754
  device.Assemble()
1755
  return device
1756

    
1757

    
1758
def Create(dev_type, unique_id, children, size):
1759
  """Create a device.
1760

1761
  """
1762
  if dev_type not in DEV_MAP:
1763
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1764
  device = DEV_MAP[dev_type].Create(unique_id, children, size)
1765
  return device