Statistics
| Branch: | Tag: | Revision:

root / lib / bdev.py @ 464f8daf

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

    
323
    # The size constraint should have been checked from the master before
324
    # calling the create function.
325
    if free_size < size:
326
      _ThrowError("Not enough free space: required %s,"
327
                  " available %s", size, free_size)
328
    result = utils.RunCmd(["lvcreate", "-L%dm" % size, "-n%s" % lv_name,
329
                           vg_name] + pvlist)
330
    if result.failed:
331
      _ThrowError("LV create failed (%s): %s",
332
                  result.fail_reason, result.output)
333
    return LogicalVolume(unique_id, children, size)
334

    
335
  @staticmethod
336
  def GetPVInfo(vg_name):
337
    """Get the free space info for PVs in a volume group.
338

339
    @param vg_name: the volume group name
340

341
    @rtype: list
342
    @return: list of tuples (free_space, name) with free_space in mebibytes
343

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

    
364
    return data
365

    
366
  def Remove(self):
367
    """Remove this logical volume.
368

369
    """
370
    if not self.minor and not self.Attach():
371
      # the LV does not exist
372
      return
373
    result = utils.RunCmd(["lvremove", "-f", "%s/%s" %
374
                           (self._vg_name, self._lv_name)])
375
    if result.failed:
376
      _ThrowError("Can't lvremove: %s - %s", result.fail_reason, result.output)
377

    
378
  def Rename(self, new_id):
379
    """Rename this logical volume.
380

381
    """
382
    if not isinstance(new_id, (tuple, list)) or len(new_id) != 2:
383
      raise errors.ProgrammerError("Invalid new logical id '%s'" % new_id)
384
    new_vg, new_name = new_id
385
    if new_vg != self._vg_name:
386
      raise errors.ProgrammerError("Can't move a logical volume across"
387
                                   " volume groups (from %s to to %s)" %
388
                                   (self._vg_name, new_vg))
389
    result = utils.RunCmd(["lvrename", new_vg, self._lv_name, new_name])
390
    if result.failed:
391
      _ThrowError("Failed to rename the logical volume: %s", result.output)
392
    self._lv_name = new_name
393
    self.dev_path = "/dev/%s/%s" % (self._vg_name, self._lv_name)
394

    
395
  def Attach(self):
396
    """Attach to an existing LV.
397

398
    This method will try to see if an existing and active LV exists
399
    which matches our name. If so, its major/minor will be
400
    recorded.
401

402
    """
403
    self.attached = False
404
    result = utils.RunCmd(["lvs", "--noheadings", "--separator=,",
405
                           "-olv_attr,lv_kernel_major,lv_kernel_minor",
406
                           self.dev_path])
407
    if result.failed:
408
      logging.error("Can't find LV %s: %s, %s",
409
                    self.dev_path, result.fail_reason, result.output)
410
      return False
411
    out = result.stdout.strip().rstrip(',')
412
    out = out.split(",")
413
    if len(out) != 3:
414
      logging.error("Can't parse LVS output, len(%s) != 3", str(out))
415
      return False
416

    
417
    status, major, minor = out[:3]
418
    if len(status) != 6:
419
      logging.error("lvs lv_attr is not 6 characters (%s)", status)
420
      return False
421

    
422
    try:
423
      major = int(major)
424
      minor = int(minor)
425
    except ValueError, err:
426
      logging.error("lvs major/minor cannot be parsed: %s", str(err))
427

    
428
    self.major = major
429
    self.minor = minor
430
    self._degraded = status[0] == 'v' # virtual volume, i.e. doesn't backing
431
                                      # storage
432
    self.attached = True
433
    return True
434

    
435
  def Assemble(self):
436
    """Assemble the device.
437

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

442
    """
443
    result = utils.RunCmd(["lvchange", "-ay", self.dev_path])
444
    if result.failed:
445
      _ThrowError("Can't activate lv %s: %s", self.dev_path, result.output)
446

    
447
  def Shutdown(self):
448
    """Shutdown the device.
449

450
    This is a no-op for the LV device type, as we don't deactivate the
451
    volumes on shutdown.
452

453
    """
454
    pass
455

    
456
  def GetSyncStatus(self):
457
    """Returns the sync status of the device.
458

459
    If this device is a mirroring device, this function returns the
460
    status of the mirror.
461

462
    For logical volumes, sync_percent and estimated_time are always
463
    None (no recovery in progress, as we don't handle the mirrored LV
464
    case). The is_degraded parameter is the inverse of the ldisk
465
    parameter.
466

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

473
    The status was already read in Attach, so we just return it.
474

475
    @rtype: tuple
476
    @return: (sync_percent, estimated_time, is_degraded, ldisk)
477

478
    """
479
    return None, None, self._degraded, self._degraded
480

    
481
  def Open(self, force=False):
482
    """Make the device ready for I/O.
483

484
    This is a no-op for the LV device type.
485

486
    """
487
    pass
488

    
489
  def Close(self):
490
    """Notifies that the device will no longer be used for I/O.
491

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

494
    """
495
    pass
496

    
497
  def Snapshot(self, size):
498
    """Create a snapshot copy of an lvm block device.
499

500
    """
501
    snap_name = self._lv_name + ".snap"
502

    
503
    # remove existing snapshot if found
504
    snap = LogicalVolume((self._vg_name, snap_name), None, size)
505
    _IgnoreError(snap.Remove)
506

    
507
    pvs_info = self.GetPVInfo(self._vg_name)
508
    if not pvs_info:
509
      _ThrowError("Can't compute PV info for vg %s", self._vg_name)
510
    pvs_info.sort()
511
    pvs_info.reverse()
512
    free_size, pv_name = pvs_info[0]
513
    if free_size < size:
514
      _ThrowError("Not enough free space: required %s,"
515
                  " available %s", size, free_size)
516

    
517
    result = utils.RunCmd(["lvcreate", "-L%dm" % size, "-s",
518
                           "-n%s" % snap_name, self.dev_path])
519
    if result.failed:
520
      _ThrowError("command: %s error: %s - %s",
521
                  result.cmd, result.fail_reason, result.output)
522

    
523
    return snap_name
524

    
525
  def SetInfo(self, text):
526
    """Update metadata with info text.
527

528
    """
529
    BlockDev.SetInfo(self, text)
530

    
531
    # Replace invalid characters
532
    text = re.sub('^[^A-Za-z0-9_+.]', '_', text)
533
    text = re.sub('[^-A-Za-z0-9_+.]', '_', text)
534

    
535
    # Only up to 128 characters are allowed
536
    text = text[:128]
537

    
538
    result = utils.RunCmd(["lvchange", "--addtag", text,
539
                           self.dev_path])
540
    if result.failed:
541
      _ThrowError("Command: %s error: %s - %s", result.cmd, result.fail_reason,
542
                  result.output)
543

    
544
  def Grow(self, amount):
545
    """Grow the logical volume.
546

547
    """
548
    # we try multiple algorithms since the 'best' ones might not have
549
    # space available in the right place, but later ones might (since
550
    # they have less constraints); also note that only recent LVM
551
    # supports 'cling'
552
    for alloc_policy in "contiguous", "cling", "normal":
553
      result = utils.RunCmd(["lvextend", "--alloc", alloc_policy,
554
                             "-L", "+%dm" % amount, self.dev_path])
555
      if not result.failed:
556
        return
557
    _ThrowError("Can't grow LV %s: %s", self.dev_path, result.output)
558

    
559

    
560
class DRBD8Status(object):
561
  """A DRBD status representation class.
562

563
  Note that this doesn't support unconfigured devices (cs:Unconfigured).
564

565
  """
566
  UNCONF_RE = re.compile(r"\s*[0-9]+:\s*cs:Unconfigured$")
567
  LINE_RE = re.compile(r"\s*[0-9]+:\s*cs:(\S+)\s+(?:st|ro):([^/]+)/(\S+)"
568
                       "\s+ds:([^/]+)/(\S+)\s+.*$")
569
  SYNC_RE = re.compile(r"^.*\ssync'ed:\s*([0-9.]+)%.*"
570
                       "\sfinish: ([0-9]+):([0-9]+):([0-9]+)\s.*$")
571

    
572
  def __init__(self, procline):
573
    u = self.UNCONF_RE.match(procline)
574
    if u:
575
      self.cstatus = "Unconfigured"
576
      self.lrole = self.rrole = self.ldisk = self.rdisk = None
577
    else:
578
      m = self.LINE_RE.match(procline)
579
      if not m:
580
        raise errors.BlockDeviceError("Can't parse input data '%s'" % procline)
581
      self.cstatus = m.group(1)
582
      self.lrole = m.group(2)
583
      self.rrole = m.group(3)
584
      self.ldisk = m.group(4)
585
      self.rdisk = m.group(5)
586

    
587
    # end reading of data from the LINE_RE or UNCONF_RE
588

    
589
    self.is_standalone = self.cstatus == "StandAlone"
590
    self.is_wfconn = self.cstatus == "WFConnection"
591
    self.is_connected = self.cstatus == "Connected"
592
    self.is_primary = self.lrole == "Primary"
593
    self.is_secondary = self.lrole == "Secondary"
594
    self.peer_primary = self.rrole == "Primary"
595
    self.peer_secondary = self.rrole == "Secondary"
596
    self.both_primary = self.is_primary and self.peer_primary
597
    self.both_secondary = self.is_secondary and self.peer_secondary
598

    
599
    self.is_diskless = self.ldisk == "Diskless"
600
    self.is_disk_uptodate = self.ldisk == "UpToDate"
601

    
602
    self.is_in_resync = self.cstatus in ("SyncSource", "SyncTarget")
603
    self.is_in_use = self.cstatus != "Unconfigured"
604

    
605
    m = self.SYNC_RE.match(procline)
606
    if m:
607
      self.sync_percent = float(m.group(1))
608
      hours = int(m.group(2))
609
      minutes = int(m.group(3))
610
      seconds = int(m.group(4))
611
      self.est_time = hours * 3600 + minutes * 60 + seconds
612
    else:
613
      self.sync_percent = None
614
      self.est_time = None
615

    
616
    self.is_sync_target = self.peer_sync_source = self.cstatus == "SyncTarget"
617
    self.peer_sync_target = self.is_sync_source = self.cstatus == "SyncSource"
618
    self.is_resync = self.is_sync_target or self.is_sync_source
619

    
620

    
621
class BaseDRBD(BlockDev):
622
  """Base DRBD class.
623

624
  This class contains a few bits of common functionality between the
625
  0.7 and 8.x versions of DRBD.
626

627
  """
628
  _VERSION_RE = re.compile(r"^version: (\d+)\.(\d+)\.(\d+)"
629
                           r" \(api:(\d+)/proto:(\d+)(?:-(\d+))?\)")
630

    
631
  _DRBD_MAJOR = 147
632
  _ST_UNCONFIGURED = "Unconfigured"
633
  _ST_WFCONNECTION = "WFConnection"
634
  _ST_CONNECTED = "Connected"
635

    
636
  _STATUS_FILE = "/proc/drbd"
637

    
638
  @staticmethod
639
  def _GetProcData(filename=_STATUS_FILE):
640
    """Return data from /proc/drbd.
641

642
    """
643
    try:
644
      stat = open(filename, "r")
645
      try:
646
        data = stat.read().splitlines()
647
      finally:
648
        stat.close()
649
    except EnvironmentError, err:
650
      if err.errno == errno.ENOENT:
651
        _ThrowError("The file %s cannot be opened, check if the module"
652
                    " is loaded (%s)", filename, str(err))
653
      else:
654
        _ThrowError("Can't read the DRBD proc file %s: %s", filename, str(err))
655
    if not data:
656
      _ThrowError("Can't read any data from %s", filename)
657
    return data
658

    
659
  @staticmethod
660
  def _MassageProcData(data):
661
    """Transform the output of _GetProdData into a nicer form.
662

663
    @return: a dictionary of minor: joined lines from /proc/drbd
664
        for that minor
665

666
    """
667
    lmatch = re.compile("^ *([0-9]+):.*$")
668
    results = {}
669
    old_minor = old_line = None
670
    for line in data:
671
      lresult = lmatch.match(line)
672
      if lresult is not None:
673
        if old_minor is not None:
674
          results[old_minor] = old_line
675
        old_minor = int(lresult.group(1))
676
        old_line = line
677
      else:
678
        if old_minor is not None:
679
          old_line += " " + line.strip()
680
    # add last line
681
    if old_minor is not None:
682
      results[old_minor] = old_line
683
    return results
684

    
685
  @classmethod
686
  def _GetVersion(cls):
687
    """Return the DRBD version.
688

689
    This will return a dict with keys:
690
      - k_major
691
      - k_minor
692
      - k_point
693
      - api
694
      - proto
695
      - proto2 (only on drbd > 8.2.X)
696

697
    """
698
    proc_data = cls._GetProcData()
699
    first_line = proc_data[0].strip()
700
    version = cls._VERSION_RE.match(first_line)
701
    if not version:
702
      raise errors.BlockDeviceError("Can't parse DRBD version from '%s'" %
703
                                    first_line)
704

    
705
    values = version.groups()
706
    retval = {'k_major': int(values[0]),
707
              'k_minor': int(values[1]),
708
              'k_point': int(values[2]),
709
              'api': int(values[3]),
710
              'proto': int(values[4]),
711
             }
712
    if values[5] is not None:
713
      retval['proto2'] = values[5]
714

    
715
    return retval
716

    
717
  @staticmethod
718
  def _DevPath(minor):
719
    """Return the path to a drbd device for a given minor.
720

721
    """
722
    return "/dev/drbd%d" % minor
723

    
724
  @classmethod
725
  def GetUsedDevs(cls):
726
    """Compute the list of used DRBD devices.
727

728
    """
729
    data = cls._GetProcData()
730

    
731
    used_devs = {}
732
    valid_line = re.compile("^ *([0-9]+): cs:([^ ]+).*$")
733
    for line in data:
734
      match = valid_line.match(line)
735
      if not match:
736
        continue
737
      minor = int(match.group(1))
738
      state = match.group(2)
739
      if state == cls._ST_UNCONFIGURED:
740
        continue
741
      used_devs[minor] = state, line
742

    
743
    return used_devs
744

    
745
  def _SetFromMinor(self, minor):
746
    """Set our parameters based on the given minor.
747

748
    This sets our minor variable and our dev_path.
749

750
    """
751
    if minor is None:
752
      self.minor = self.dev_path = None
753
      self.attached = False
754
    else:
755
      self.minor = minor
756
      self.dev_path = self._DevPath(minor)
757
      self.attached = True
758

    
759
  @staticmethod
760
  def _CheckMetaSize(meta_device):
761
    """Check if the given meta device looks like a valid one.
762

763
    This currently only check the size, which must be around
764
    128MiB.
765

766
    """
767
    result = utils.RunCmd(["blockdev", "--getsize", meta_device])
768
    if result.failed:
769
      _ThrowError("Failed to get device size: %s - %s",
770
                  result.fail_reason, result.output)
771
    try:
772
      sectors = int(result.stdout)
773
    except ValueError:
774
      _ThrowError("Invalid output from blockdev: '%s'", result.stdout)
775
    bytes = sectors * 512
776
    if bytes < 128 * 1024 * 1024: # less than 128MiB
777
      _ThrowError("Meta device too small (%.2fMib)", (bytes / 1024 / 1024))
778
    if bytes > (128 + 32) * 1024 * 1024: # account for an extra (big) PE on LVM
779
      _ThrowError("Meta device too big (%.2fMiB)", (bytes / 1024 / 1024))
780

    
781
  def Rename(self, new_id):
782
    """Rename a device.
783

784
    This is not supported for drbd devices.
785

786
    """
787
    raise errors.ProgrammerError("Can't rename a drbd device")
788

    
789

    
790
class DRBD8(BaseDRBD):
791
  """DRBD v8.x block device.
792

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

797
  The unique_id for the drbd device is the (local_ip, local_port,
798
  remote_ip, remote_port) tuple, and it must have two children: the
799
  data device and the meta_device. The meta device is checked for
800
  valid size and is zeroed on create.
801

802
  """
803
  _MAX_MINORS = 255
804
  _PARSE_SHOW = None
805

    
806
  # timeout constants
807
  _NET_RECONFIG_TIMEOUT = 60
808

    
809
  def __init__(self, unique_id, children, size):
810
    if children and children.count(None) > 0:
811
      children = []
812
    super(DRBD8, self).__init__(unique_id, children, size)
813
    self.major = self._DRBD_MAJOR
814
    version = self._GetVersion()
815
    if version['k_major'] != 8 :
816
      _ThrowError("Mismatch in DRBD kernel version and requested ganeti"
817
                  " usage: kernel is %s.%s, ganeti wants 8.x",
818
                  version['k_major'], version['k_minor'])
819

    
820
    if len(children) not in (0, 2):
821
      raise ValueError("Invalid configuration data %s" % str(children))
822
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 6:
823
      raise ValueError("Invalid configuration data %s" % str(unique_id))
824
    (self._lhost, self._lport,
825
     self._rhost, self._rport,
826
     self._aminor, self._secret) = unique_id
827
    if (self._lhost is not None and self._lhost == self._rhost and
828
        self._lport == self._rport):
829
      raise ValueError("Invalid configuration data, same local/remote %s" %
830
                       (unique_id,))
831
    self.Attach()
832

    
833
  @classmethod
834
  def _InitMeta(cls, minor, dev_path):
835
    """Initialize a meta device.
836

837
    This will not work if the given minor is in use.
838

839
    """
840
    result = utils.RunCmd(["drbdmeta", "--force", cls._DevPath(minor),
841
                           "v08", dev_path, "0", "create-md"])
842
    if result.failed:
843
      _ThrowError("Can't initialize meta device: %s", result.output)
844

    
845
  @classmethod
846
  def _FindUnusedMinor(cls):
847
    """Find an unused DRBD device.
848

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

852
    """
853
    data = cls._GetProcData()
854

    
855
    unused_line = re.compile("^ *([0-9]+): cs:Unconfigured$")
856
    used_line = re.compile("^ *([0-9]+): cs:")
857
    highest = None
858
    for line in data:
859
      match = unused_line.match(line)
860
      if match:
861
        return int(match.group(1))
862
      match = used_line.match(line)
863
      if match:
864
        minor = int(match.group(1))
865
        highest = max(highest, minor)
866
    if highest is None: # there are no minors in use at all
867
      return 0
868
    if highest >= cls._MAX_MINORS:
869
      logging.error("Error: no free drbd minors!")
870
      raise errors.BlockDeviceError("Can't find a free DRBD minor")
871
    return highest + 1
872

    
873
  @classmethod
874
  def _GetShowParser(cls):
875
    """Return a parser for `drbd show` output.
876

877
    This will either create or return an already-create parser for the
878
    output of the command `drbd show`.
879

880
    """
881
    if cls._PARSE_SHOW is not None:
882
      return cls._PARSE_SHOW
883

    
884
    # pyparsing setup
885
    lbrace = pyp.Literal("{").suppress()
886
    rbrace = pyp.Literal("}").suppress()
887
    semi = pyp.Literal(";").suppress()
888
    # this also converts the value to an int
889
    number = pyp.Word(pyp.nums).setParseAction(lambda s, l, t: int(t[0]))
890

    
891
    comment = pyp.Literal ("#") + pyp.Optional(pyp.restOfLine)
892
    defa = pyp.Literal("_is_default").suppress()
893
    dbl_quote = pyp.Literal('"').suppress()
894

    
895
    keyword = pyp.Word(pyp.alphanums + '-')
896

    
897
    # value types
898
    value = pyp.Word(pyp.alphanums + '_-/.:')
899
    quoted = dbl_quote + pyp.CharsNotIn('"') + dbl_quote
900
    addr_type = (pyp.Optional(pyp.Literal("ipv4")).suppress() +
901
                 pyp.Optional(pyp.Literal("ipv6")).suppress())
902
    addr_port = (addr_type + pyp.Word(pyp.nums + '.') +
903
                 pyp.Literal(':').suppress() + number)
904
    # meta device, extended syntax
905
    meta_value = ((value ^ quoted) + pyp.Literal('[').suppress() +
906
                  number + pyp.Word(']').suppress())
907
    # device name, extended syntax
908
    device_value = pyp.Literal("minor").suppress() + number
909

    
910
    # a statement
911
    stmt = (~rbrace + keyword + ~lbrace +
912
            pyp.Optional(addr_port ^ value ^ quoted ^ meta_value ^
913
                         device_value) +
914
            pyp.Optional(defa) + semi +
915
            pyp.Optional(pyp.restOfLine).suppress())
916

    
917
    # an entire section
918
    section_name = pyp.Word(pyp.alphas + '_')
919
    section = section_name + lbrace + pyp.ZeroOrMore(pyp.Group(stmt)) + rbrace
920

    
921
    bnf = pyp.ZeroOrMore(pyp.Group(section ^ stmt))
922
    bnf.ignore(comment)
923

    
924
    cls._PARSE_SHOW = bnf
925

    
926
    return bnf
927

    
928
  @classmethod
929
  def _GetShowData(cls, minor):
930
    """Return the `drbdsetup show` data for a minor.
931

932
    """
933
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "show"])
934
    if result.failed:
935
      logging.error("Can't display the drbd config: %s - %s",
936
                    result.fail_reason, result.output)
937
      return None
938
    return result.stdout
939

    
940
  @classmethod
941
  def _GetDevInfo(cls, out):
942
    """Parse details about a given DRBD minor.
943

944
    This return, if available, the local backing device (as a path)
945
    and the local and remote (ip, port) information from a string
946
    containing the output of the `drbdsetup show` command as returned
947
    by _GetShowData.
948

949
    """
950
    data = {}
951
    if not out:
952
      return data
953

    
954
    bnf = cls._GetShowParser()
955
    # run pyparse
956

    
957
    try:
958
      results = bnf.parseString(out)
959
    except pyp.ParseException, err:
960
      _ThrowError("Can't parse drbdsetup show output: %s", str(err))
961

    
962
    # and massage the results into our desired format
963
    for section in results:
964
      sname = section[0]
965
      if sname == "_this_host":
966
        for lst in section[1:]:
967
          if lst[0] == "disk":
968
            data["local_dev"] = lst[1]
969
          elif lst[0] == "meta-disk":
970
            data["meta_dev"] = lst[1]
971
            data["meta_index"] = lst[2]
972
          elif lst[0] == "address":
973
            data["local_addr"] = tuple(lst[1:])
974
      elif sname == "_remote_host":
975
        for lst in section[1:]:
976
          if lst[0] == "address":
977
            data["remote_addr"] = tuple(lst[1:])
978
    return data
979

    
980
  def _MatchesLocal(self, info):
981
    """Test if our local config matches with an existing device.
982

983
    The parameter should be as returned from `_GetDevInfo()`. This
984
    method tests if our local backing device is the same as the one in
985
    the info parameter, in effect testing if we look like the given
986
    device.
987

988
    """
989
    if self._children:
990
      backend, meta = self._children
991
    else:
992
      backend = meta = None
993

    
994
    if backend is not None:
995
      retval = ("local_dev" in info and info["local_dev"] == backend.dev_path)
996
    else:
997
      retval = ("local_dev" not in info)
998

    
999
    if meta is not None:
1000
      retval = retval and ("meta_dev" in info and
1001
                           info["meta_dev"] == meta.dev_path)
1002
      retval = retval and ("meta_index" in info and
1003
                           info["meta_index"] == 0)
1004
    else:
1005
      retval = retval and ("meta_dev" not in info and
1006
                           "meta_index" not in info)
1007
    return retval
1008

    
1009
  def _MatchesNet(self, info):
1010
    """Test if our network config matches with an existing device.
1011

1012
    The parameter should be as returned from `_GetDevInfo()`. This
1013
    method tests if our network configuration is the same as the one
1014
    in the info parameter, in effect testing if we look like the given
1015
    device.
1016

1017
    """
1018
    if (((self._lhost is None and not ("local_addr" in info)) and
1019
         (self._rhost is None and not ("remote_addr" in info)))):
1020
      return True
1021

    
1022
    if self._lhost is None:
1023
      return False
1024

    
1025
    if not ("local_addr" in info and
1026
            "remote_addr" in info):
1027
      return False
1028

    
1029
    retval = (info["local_addr"] == (self._lhost, self._lport))
1030
    retval = (retval and
1031
              info["remote_addr"] == (self._rhost, self._rport))
1032
    return retval
1033

    
1034
  @classmethod
1035
  def _AssembleLocal(cls, minor, backend, meta):
1036
    """Configure the local part of a DRBD device.
1037

1038
    """
1039
    args = ["drbdsetup", cls._DevPath(minor), "disk",
1040
            backend, meta, "0", "-e", "detach", "--create-device"]
1041
    result = utils.RunCmd(args)
1042
    if result.failed:
1043
      _ThrowError("drbd%d: can't attach local disk: %s", minor, result.output)
1044

    
1045
  @classmethod
1046
  def _AssembleNet(cls, minor, net_info, protocol,
1047
                   dual_pri=False, hmac=None, secret=None):
1048
    """Configure the network part of the device.
1049

1050
    """
1051
    lhost, lport, rhost, rport = net_info
1052
    if None in net_info:
1053
      # we don't want network connection and actually want to make
1054
      # sure its shutdown
1055
      cls._ShutdownNet(minor)
1056
      return
1057

    
1058
    # Workaround for a race condition. When DRBD is doing its dance to
1059
    # establish a connection with its peer, it also sends the
1060
    # synchronization speed over the wire. In some cases setting the
1061
    # sync speed only after setting up both sides can race with DRBD
1062
    # connecting, hence we set it here before telling DRBD anything
1063
    # about its peer.
1064
    cls._SetMinorSyncSpeed(minor, constants.SYNC_SPEED)
1065

    
1066
    args = ["drbdsetup", cls._DevPath(minor), "net",
1067
            "%s:%s" % (lhost, lport), "%s:%s" % (rhost, rport), protocol,
1068
            "-A", "discard-zero-changes",
1069
            "-B", "consensus",
1070
            "--create-device",
1071
            ]
1072
    if dual_pri:
1073
      args.append("-m")
1074
    if hmac and secret:
1075
      args.extend(["-a", hmac, "-x", secret])
1076
    result = utils.RunCmd(args)
1077
    if result.failed:
1078
      _ThrowError("drbd%d: can't setup network: %s - %s",
1079
                  minor, result.fail_reason, result.output)
1080

    
1081
    timeout = time.time() + 10
1082
    ok = False
1083
    while time.time() < timeout:
1084
      info = cls._GetDevInfo(cls._GetShowData(minor))
1085
      if not "local_addr" in info or not "remote_addr" in info:
1086
        time.sleep(1)
1087
        continue
1088
      if (info["local_addr"] != (lhost, lport) or
1089
          info["remote_addr"] != (rhost, rport)):
1090
        time.sleep(1)
1091
        continue
1092
      ok = True
1093
      break
1094
    if not ok:
1095
      _ThrowError("drbd%d: timeout while configuring network", minor)
1096

    
1097
  def AddChildren(self, devices):
1098
    """Add a disk to the DRBD device.
1099

1100
    """
1101
    if self.minor is None:
1102
      _ThrowError("drbd%d: can't attach to dbrd8 during AddChildren",
1103
                  self._aminor)
1104
    if len(devices) != 2:
1105
      _ThrowError("drbd%d: need two devices for AddChildren", self.minor)
1106
    info = self._GetDevInfo(self._GetShowData(self.minor))
1107
    if "local_dev" in info:
1108
      _ThrowError("drbd%d: already attached to a local disk", self.minor)
1109
    backend, meta = devices
1110
    if backend.dev_path is None or meta.dev_path is None:
1111
      _ThrowError("drbd%d: children not ready during AddChildren", self.minor)
1112
    backend.Open()
1113
    meta.Open()
1114
    self._CheckMetaSize(meta.dev_path)
1115
    self._InitMeta(self._FindUnusedMinor(), meta.dev_path)
1116

    
1117
    self._AssembleLocal(self.minor, backend.dev_path, meta.dev_path)
1118
    self._children = devices
1119

    
1120
  def RemoveChildren(self, devices):
1121
    """Detach the drbd device from local storage.
1122

1123
    """
1124
    if self.minor is None:
1125
      _ThrowError("drbd%d: can't attach to drbd8 during RemoveChildren",
1126
                  self._aminor)
1127
    # early return if we don't actually have backing storage
1128
    info = self._GetDevInfo(self._GetShowData(self.minor))
1129
    if "local_dev" not in info:
1130
      return
1131
    if len(self._children) != 2:
1132
      _ThrowError("drbd%d: we don't have two children: %s", self.minor,
1133
                  self._children)
1134
    if self._children.count(None) == 2: # we don't actually have children :)
1135
      logging.warning("drbd%d: requested detach while detached", self.minor)
1136
      return
1137
    if len(devices) != 2:
1138
      _ThrowError("drbd%d: we need two children in RemoveChildren", self.minor)
1139
    for child, dev in zip(self._children, devices):
1140
      if dev != child.dev_path:
1141
        _ThrowError("drbd%d: mismatch in local storage (%s != %s) in"
1142
                    " RemoveChildren", self.minor, dev, child.dev_path)
1143

    
1144
    self._ShutdownLocal(self.minor)
1145
    self._children = []
1146

    
1147
  @classmethod
1148
  def _SetMinorSyncSpeed(cls, minor, kbytes):
1149
    """Set the speed of the DRBD syncer.
1150

1151
    This is the low-level implementation.
1152

1153
    @type minor: int
1154
    @param minor: the drbd minor whose settings we change
1155
    @type kbytes: int
1156
    @param kbytes: the speed in kbytes/second
1157
    @rtype: boolean
1158
    @return: the success of the operation
1159

1160
    """
1161
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "syncer",
1162
                           "-r", "%d" % kbytes, "--create-device"])
1163
    if result.failed:
1164
      logging.error("Can't change syncer rate: %s - %s",
1165
                    result.fail_reason, result.output)
1166
    return not result.failed
1167

    
1168
  def SetSyncSpeed(self, kbytes):
1169
    """Set the speed of the DRBD syncer.
1170

1171
    @type kbytes: int
1172
    @param kbytes: the speed in kbytes/second
1173
    @rtype: boolean
1174
    @return: the success of the operation
1175

1176
    """
1177
    if self.minor is None:
1178
      logging.info("Not attached during SetSyncSpeed")
1179
      return False
1180
    children_result = super(DRBD8, self).SetSyncSpeed(kbytes)
1181
    return self._SetMinorSyncSpeed(self.minor, kbytes) and children_result
1182

    
1183
  def GetProcStatus(self):
1184
    """Return device data from /proc.
1185

1186
    """
1187
    if self.minor is None:
1188
      _ThrowError("drbd%d: GetStats() called while not attached", self._aminor)
1189
    proc_info = self._MassageProcData(self._GetProcData())
1190
    if self.minor not in proc_info:
1191
      _ThrowError("drbd%d: can't find myself in /proc", self.minor)
1192
    return DRBD8Status(proc_info[self.minor])
1193

    
1194
  def GetSyncStatus(self):
1195
    """Returns the sync status of the device.
1196

1197

1198
    If sync_percent is None, it means all is ok
1199
    If estimated_time is None, it means we can't esimate
1200
    the time needed, otherwise it's the time left in seconds.
1201

1202

1203
    We set the is_degraded parameter to True on two conditions:
1204
    network not connected or local disk missing.
1205

1206
    We compute the ldisk parameter based on wheter we have a local
1207
    disk or not.
1208

1209
    @rtype: tuple
1210
    @return: (sync_percent, estimated_time, is_degraded, ldisk)
1211

1212
    """
1213
    if self.minor is None and not self.Attach():
1214
      _ThrowError("drbd%d: can't Attach() in GetSyncStatus", self._aminor)
1215
    stats = self.GetProcStatus()
1216
    ldisk = not stats.is_disk_uptodate
1217
    is_degraded = not stats.is_connected
1218
    return stats.sync_percent, stats.est_time, is_degraded or ldisk, ldisk
1219

    
1220
  def Open(self, force=False):
1221
    """Make the local state primary.
1222

1223
    If the 'force' parameter is given, the '-o' option is passed to
1224
    drbdsetup. Since this is a potentially dangerous operation, the
1225
    force flag should be only given after creation, when it actually
1226
    is mandatory.
1227

1228
    """
1229
    if self.minor is None and not self.Attach():
1230
      logging.error("DRBD cannot attach to a device during open")
1231
      return False
1232
    cmd = ["drbdsetup", self.dev_path, "primary"]
1233
    if force:
1234
      cmd.append("-o")
1235
    result = utils.RunCmd(cmd)
1236
    if result.failed:
1237
      _ThrowError("drbd%d: can't make drbd device primary: %s", self.minor,
1238
                  result.output)
1239

    
1240
  def Close(self):
1241
    """Make the local state secondary.
1242

1243
    This will, of course, fail if the device is in use.
1244

1245
    """
1246
    if self.minor is None and not self.Attach():
1247
      _ThrowError("drbd%d: can't Attach() in Close()", self._aminor)
1248
    result = utils.RunCmd(["drbdsetup", self.dev_path, "secondary"])
1249
    if result.failed:
1250
      _ThrowError("drbd%d: can't switch drbd device to secondary: %s",
1251
                  self.minor, result.output)
1252

    
1253
  def DisconnectNet(self):
1254
    """Removes network configuration.
1255

1256
    This method shutdowns the network side of the device.
1257

1258
    The method will wait up to a hardcoded timeout for the device to
1259
    go into standalone after the 'disconnect' command before
1260
    re-configuring it, as sometimes it takes a while for the
1261
    disconnect to actually propagate and thus we might issue a 'net'
1262
    command while the device is still connected. If the device will
1263
    still be attached to the network and we time out, we raise an
1264
    exception.
1265

1266
    """
1267
    if self.minor is None:
1268
      _ThrowError("drbd%d: disk not attached in re-attach net", self._aminor)
1269

    
1270
    if None in (self._lhost, self._lport, self._rhost, self._rport):
1271
      _ThrowError("drbd%d: DRBD disk missing network info in"
1272
                  " DisconnectNet()", self.minor)
1273

    
1274
    ever_disconnected = _IgnoreError(self._ShutdownNet, self.minor)
1275
    timeout_limit = time.time() + self._NET_RECONFIG_TIMEOUT
1276
    sleep_time = 0.100 # we start the retry time at 100 miliseconds
1277
    while time.time() < timeout_limit:
1278
      status = self.GetProcStatus()
1279
      if status.is_standalone:
1280
        break
1281
      # retry the disconnect, it seems possible that due to a
1282
      # well-time disconnect on the peer, my disconnect command might
1283
      # be ingored and forgotten
1284
      ever_disconnected = _IgnoreError(self._ShutdownNet, self.minor) or \
1285
                          ever_disconnected
1286
      time.sleep(sleep_time)
1287
      sleep_time = min(2, sleep_time * 1.5)
1288

    
1289
    if not status.is_standalone:
1290
      if ever_disconnected:
1291
        msg = ("drbd%d: device did not react to the"
1292
               " 'disconnect' command in a timely manner")
1293
      else:
1294
        msg = "drbd%d: can't shutdown network, even after multiple retries"
1295
      _ThrowError(msg, self.minor)
1296

    
1297
    reconfig_time = time.time() - timeout_limit + self._NET_RECONFIG_TIMEOUT
1298
    if reconfig_time > 15: # hardcoded alert limit
1299
      logging.info("drbd%d: DisconnectNet: detach took %.3f seconds",
1300
                   self.minor, reconfig_time)
1301

    
1302
  def AttachNet(self, multimaster):
1303
    """Reconnects the network.
1304

1305
    This method connects the network side of the device with a
1306
    specified multi-master flag. The device needs to be 'Standalone'
1307
    but have valid network configuration data.
1308

1309
    Args:
1310
      - multimaster: init the network in dual-primary mode
1311

1312
    """
1313
    if self.minor is None:
1314
      _ThrowError("drbd%d: device not attached in AttachNet", self._aminor)
1315

    
1316
    if None in (self._lhost, self._lport, self._rhost, self._rport):
1317
      _ThrowError("drbd%d: missing network info in AttachNet()", self.minor)
1318

    
1319
    status = self.GetProcStatus()
1320

    
1321
    if not status.is_standalone:
1322
      _ThrowError("drbd%d: device is not standalone in AttachNet", self.minor)
1323

    
1324
    self._AssembleNet(self.minor,
1325
                      (self._lhost, self._lport, self._rhost, self._rport),
1326
                      constants.DRBD_NET_PROTOCOL, dual_pri=multimaster,
1327
                      hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1328

    
1329
  def Attach(self):
1330
    """Check if our minor is configured.
1331

1332
    This doesn't do any device configurations - it only checks if the
1333
    minor is in a state different from Unconfigured.
1334

1335
    Note that this function will not change the state of the system in
1336
    any way (except in case of side-effects caused by reading from
1337
    /proc).
1338

1339
    """
1340
    used_devs = self.GetUsedDevs()
1341
    if self._aminor in used_devs:
1342
      minor = self._aminor
1343
    else:
1344
      minor = None
1345

    
1346
    self._SetFromMinor(minor)
1347
    return minor is not None
1348

    
1349
  def Assemble(self):
1350
    """Assemble the drbd.
1351

1352
    Method:
1353
      - if we have a configured device, we try to ensure that it matches
1354
        our config
1355
      - if not, we create it from zero
1356

1357
    """
1358
    super(DRBD8, self).Assemble()
1359

    
1360
    self.Attach()
1361
    if self.minor is None:
1362
      # local device completely unconfigured
1363
      self._FastAssemble()
1364
    else:
1365
      # we have to recheck the local and network status and try to fix
1366
      # the device
1367
      self._SlowAssemble()
1368

    
1369
  def _SlowAssemble(self):
1370
    """Assembles the DRBD device from a (partially) configured device.
1371

1372
    In case of partially attached (local device matches but no network
1373
    setup), we perform the network attach. If successful, we re-test
1374
    the attach if can return success.
1375

1376
    """
1377
    net_data = (self._lhost, self._lport, self._rhost, self._rport)
1378
    for minor in (self._aminor,):
1379
      info = self._GetDevInfo(self._GetShowData(minor))
1380
      match_l = self._MatchesLocal(info)
1381
      match_r = self._MatchesNet(info)
1382

    
1383
      if match_l and match_r:
1384
        # everything matches
1385
        break
1386

    
1387
      if match_l and not match_r and "local_addr" not in info:
1388
        # disk matches, but not attached to network, attach and recheck
1389
        self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
1390
                          hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1391
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1392
          break
1393
        else:
1394
          _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
1395
                      " show' disagrees", minor)
1396

    
1397
      if match_r and "local_dev" not in info:
1398
        # no local disk, but network attached and it matches
1399
        self._AssembleLocal(minor, self._children[0].dev_path,
1400
                            self._children[1].dev_path)
1401
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1402
          break
1403
        else:
1404
          _ThrowError("drbd%d: disk attach successful, but 'drbdsetup"
1405
                      " show' disagrees", minor)
1406

    
1407
      # this case must be considered only if we actually have local
1408
      # storage, i.e. not in diskless mode, because all diskless
1409
      # devices are equal from the point of view of local
1410
      # configuration
1411
      if (match_l and "local_dev" in info and
1412
          not match_r and "local_addr" in info):
1413
        # strange case - the device network part points to somewhere
1414
        # else, even though its local storage is ours; as we own the
1415
        # drbd space, we try to disconnect from the remote peer and
1416
        # reconnect to our correct one
1417
        try:
1418
          self._ShutdownNet(minor)
1419
        except errors.BlockDeviceError, err:
1420
          _ThrowError("drbd%d: device has correct local storage, wrong"
1421
                      " remote peer and is unable to disconnect in order"
1422
                      " to attach to the correct peer: %s", minor, str(err))
1423
        # note: _AssembleNet also handles the case when we don't want
1424
        # local storage (i.e. one or more of the _[lr](host|port) is
1425
        # None)
1426
        self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
1427
                          hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1428
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1429
          break
1430
        else:
1431
          _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
1432
                      " show' disagrees", minor)
1433

    
1434
    else:
1435
      minor = None
1436

    
1437
    self._SetFromMinor(minor)
1438
    if minor is None:
1439
      _ThrowError("drbd%d: cannot activate, unknown or unhandled reason",
1440
                  self._aminor)
1441

    
1442
  def _FastAssemble(self):
1443
    """Assemble the drbd device from zero.
1444

1445
    This is run when in Assemble we detect our minor is unused.
1446

1447
    """
1448
    minor = self._aminor
1449
    if self._children and self._children[0] and self._children[1]:
1450
      self._AssembleLocal(minor, self._children[0].dev_path,
1451
                          self._children[1].dev_path)
1452
    if self._lhost and self._lport and self._rhost and self._rport:
1453
      self._AssembleNet(minor,
1454
                        (self._lhost, self._lport, self._rhost, self._rport),
1455
                        constants.DRBD_NET_PROTOCOL,
1456
                        hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1457
    self._SetFromMinor(minor)
1458

    
1459
  @classmethod
1460
  def _ShutdownLocal(cls, minor):
1461
    """Detach from the local device.
1462

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

1466
    """
1467
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "detach"])
1468
    if result.failed:
1469
      _ThrowError("drbd%d: can't detach local disk: %s", minor, result.output)
1470

    
1471
  @classmethod
1472
  def _ShutdownNet(cls, minor):
1473
    """Disconnect from the remote peer.
1474

1475
    This fails if we don't have a local device.
1476

1477
    """
1478
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disconnect"])
1479
    if result.failed:
1480
      _ThrowError("drbd%d: can't shutdown network: %s", minor, result.output)
1481

    
1482
  @classmethod
1483
  def _ShutdownAll(cls, minor):
1484
    """Deactivate the device.
1485

1486
    This will, of course, fail if the device is in use.
1487

1488
    """
1489
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "down"])
1490
    if result.failed:
1491
      _ThrowError("drbd%d: can't shutdown drbd device: %s",
1492
                  minor, result.output)
1493

    
1494
  def Shutdown(self):
1495
    """Shutdown the DRBD device.
1496

1497
    """
1498
    if self.minor is None and not self.Attach():
1499
      logging.info("drbd%d: not attached during Shutdown()", self._aminor)
1500
      return
1501
    minor = self.minor
1502
    self.minor = None
1503
    self.dev_path = None
1504
    self._ShutdownAll(minor)
1505

    
1506
  def Remove(self):
1507
    """Stub remove for DRBD devices.
1508

1509
    """
1510
    self.Shutdown()
1511

    
1512
  @classmethod
1513
  def Create(cls, unique_id, children, size):
1514
    """Create a new DRBD8 device.
1515

1516
    Since DRBD devices are not created per se, just assembled, this
1517
    function only initializes the metadata.
1518

1519
    """
1520
    if len(children) != 2:
1521
      raise errors.ProgrammerError("Invalid setup for the drbd device")
1522
    # check that the minor is unused
1523
    aminor = unique_id[4]
1524
    proc_info = cls._MassageProcData(cls._GetProcData())
1525
    if aminor in proc_info:
1526
      status = DRBD8Status(proc_info[aminor])
1527
      in_use = status.is_in_use
1528
    else:
1529
      in_use = False
1530
    if in_use:
1531
      _ThrowError("drbd%d: minor is already in use at Create() time", aminor)
1532
    meta = children[1]
1533
    meta.Assemble()
1534
    if not meta.Attach():
1535
      _ThrowError("drbd%d: can't attach to meta device '%s'",
1536
                  aminor, meta)
1537
    cls._CheckMetaSize(meta.dev_path)
1538
    cls._InitMeta(aminor, meta.dev_path)
1539
    return cls(unique_id, children, size)
1540

    
1541
  def Grow(self, amount):
1542
    """Resize the DRBD device and its backing storage.
1543

1544
    """
1545
    if self.minor is None:
1546
      _ThrowError("drbd%d: Grow called while not attached", self._aminor)
1547
    if len(self._children) != 2 or None in self._children:
1548
      _ThrowError("drbd%d: cannot grow diskless device", self.minor)
1549
    self._children[0].Grow(amount)
1550
    result = utils.RunCmd(["drbdsetup", self.dev_path, "resize"])
1551
    if result.failed:
1552
      _ThrowError("drbd%d: resize failed: %s", self.minor, result.output)
1553

    
1554

    
1555
class FileStorage(BlockDev):
1556
  """File device.
1557

1558
  This class represents the a file storage backend device.
1559

1560
  The unique_id for the file device is a (file_driver, file_path) tuple.
1561

1562
  """
1563
  def __init__(self, unique_id, children, size):
1564
    """Initalizes a file device backend.
1565

1566
    """
1567
    if children:
1568
      raise errors.BlockDeviceError("Invalid setup for file device")
1569
    super(FileStorage, self).__init__(unique_id, children, size)
1570
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1571
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1572
    self.driver = unique_id[0]
1573
    self.dev_path = unique_id[1]
1574
    self.Attach()
1575

    
1576
  def Assemble(self):
1577
    """Assemble the device.
1578

1579
    Checks whether the file device exists, raises BlockDeviceError otherwise.
1580

1581
    """
1582
    if not os.path.exists(self.dev_path):
1583
      _ThrowError("File device '%s' does not exist" % self.dev_path)
1584

    
1585
  def Shutdown(self):
1586
    """Shutdown the device.
1587

1588
    This is a no-op for the file type, as we don't deacivate
1589
    the file on shutdown.
1590

1591
    """
1592
    pass
1593

    
1594
  def Open(self, force=False):
1595
    """Make the device ready for I/O.
1596

1597
    This is a no-op for the file type.
1598

1599
    """
1600
    pass
1601

    
1602
  def Close(self):
1603
    """Notifies that the device will no longer be used for I/O.
1604

1605
    This is a no-op for the file type.
1606

1607
    """
1608
    pass
1609

    
1610
  def Remove(self):
1611
    """Remove the file backing the block device.
1612

1613
    @rtype: boolean
1614
    @return: True if the removal was successful
1615

1616
    """
1617
    try:
1618
      os.remove(self.dev_path)
1619
    except OSError, err:
1620
      if err.errno != errno.ENOENT:
1621
        _ThrowError("Can't remove file '%s': %s", self.dev_path, err)
1622

    
1623
  def Attach(self):
1624
    """Attach to an existing file.
1625

1626
    Check if this file already exists.
1627

1628
    @rtype: boolean
1629
    @return: True if file exists
1630

1631
    """
1632
    self.attached = os.path.exists(self.dev_path)
1633
    return self.attached
1634

    
1635
  @classmethod
1636
  def Create(cls, unique_id, children, size):
1637
    """Create a new file.
1638

1639
    @param size: the size of file in MiB
1640

1641
    @rtype: L{bdev.FileStorage}
1642
    @return: an instance of FileStorage
1643

1644
    """
1645
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1646
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1647
    dev_path = unique_id[1]
1648
    if os.path.exists(dev_path):
1649
      _ThrowError("File already existing: %s", dev_path)
1650
    try:
1651
      f = open(dev_path, 'w')
1652
      f.truncate(size * 1024 * 1024)
1653
      f.close()
1654
    except IOError, err:
1655
      _ThrowError("Error in file creation: %", str(err))
1656

    
1657
    return FileStorage(unique_id, children, size)
1658

    
1659

    
1660
DEV_MAP = {
1661
  constants.LD_LV: LogicalVolume,
1662
  constants.LD_DRBD8: DRBD8,
1663
  constants.LD_FILE: FileStorage,
1664
  }
1665

    
1666

    
1667
def FindDevice(dev_type, unique_id, children, size):
1668
  """Search for an existing, assembled device.
1669

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

1673
  """
1674
  if dev_type not in DEV_MAP:
1675
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1676
  device = DEV_MAP[dev_type](unique_id, children, size)
1677
  if not device.attached:
1678
    return None
1679
  return device
1680

    
1681

    
1682
def Assemble(dev_type, unique_id, children, size):
1683
  """Try to attach or assemble an existing device.
1684

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

1688
  """
1689
  if dev_type not in DEV_MAP:
1690
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1691
  device = DEV_MAP[dev_type](unique_id, children, size)
1692
  device.Assemble()
1693
  return device
1694

    
1695

    
1696
def Create(dev_type, unique_id, children, size):
1697
  """Create a device.
1698

1699
  """
1700
  if dev_type not in DEV_MAP:
1701
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1702
  device = DEV_MAP[dev_type].Create(unique_id, children, size)
1703
  return device