Statistics
| Branch: | Tag: | Revision:

root / lib / bdev.py @ b399ce1e

History | View | Annotate | Download (53 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):
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

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

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

130
    """
131
    pass
132

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

136
    """
137
    raise NotImplementedError
138

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

142
    """
143
    raise NotImplementedError
144

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

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

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

156
    """
157
    raise NotImplementedError
158

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

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

166
    """
167
    raise NotImplementedError
168

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

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

174
    """
175
    raise NotImplementedError
176

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

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

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

186
    """
187
    raise NotImplementedError
188

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

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

196
    """
197
    raise NotImplementedError
198

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

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

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

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

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

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

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

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

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

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

233
    """
234
    return None, None, False, False
235

    
236

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

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

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

    
261

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

265
    Only supported for some device types.
266

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

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

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

276
    """
277
    raise NotImplementedError
278

    
279
  def __repr__(self):
280
    return ("<%s: unique_id: %s, children: %s, %s:%s, %s>" %
281
            (self.__class__, self.unique_id, self._children,
282
             self.major, self.minor, self.dev_path))
283

    
284

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

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

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

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

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

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

    
319
    pvlist = [ pv[1] for pv in pvs_info ]
320
    free_size = sum([ pv[0] for pv in pvs_info ])
321

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

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

338
    @param vg_name: the volume group name
339

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

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

    
363
    return data
364

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

452
    """
453
    pass
454

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

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

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

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

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

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

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

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

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

485
    """
486
    pass
487

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

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

493
    """
494
    pass
495

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

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

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

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

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

    
522
    return snap_name
523

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

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

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

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

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

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

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

    
558

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

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

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

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

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

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

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

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

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

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

    
619

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

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

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

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

    
635
  _STATUS_FILE = "/proc/drbd"
636

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

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

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

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

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

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

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

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

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

    
714
    return retval
715

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

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

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

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

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

    
742
    return used_devs
743

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

747
    This sets our minor variable and our dev_path.
748

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

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

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

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

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

783
    This is not supported for drbd devices.
784

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

    
788

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

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

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

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

    
805
  # timeout constants
806
  _NET_RECONFIG_TIMEOUT = 60
807

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
896
    # value types
897
    value = pyp.Word(pyp.alphanums + '_-/.:')
898
    quoted = dbl_quote + pyp.CharsNotIn('"') + dbl_quote
899
    addr_port = (pyp.Word(pyp.nums + '.') + pyp.Literal(':').suppress() +
900
                 number)
901
    # meta device, extended syntax
902
    meta_value = ((value ^ quoted) + pyp.Literal('[').suppress() +
903
                  number + pyp.Word(']').suppress())
904

    
905
    # a statement
906
    stmt = (~rbrace + keyword + ~lbrace +
907
            pyp.Optional(addr_port ^ value ^ quoted ^ meta_value) +
908
            pyp.Optional(defa) + semi +
909
            pyp.Optional(pyp.restOfLine).suppress())
910

    
911
    # an entire section
912
    section_name = pyp.Word(pyp.alphas + '_')
913
    section = section_name + lbrace + pyp.ZeroOrMore(pyp.Group(stmt)) + rbrace
914

    
915
    bnf = pyp.ZeroOrMore(pyp.Group(section ^ stmt))
916
    bnf.ignore(comment)
917

    
918
    cls._PARSE_SHOW = bnf
919

    
920
    return bnf
921

    
922
  @classmethod
923
  def _GetShowData(cls, minor):
924
    """Return the `drbdsetup show` data for a minor.
925

926
    """
927
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "show"])
928
    if result.failed:
929
      logging.error("Can't display the drbd config: %s - %s",
930
                    result.fail_reason, result.output)
931
      return None
932
    return result.stdout
933

    
934
  @classmethod
935
  def _GetDevInfo(cls, out):
936
    """Parse details about a given DRBD minor.
937

938
    This return, if available, the local backing device (as a path)
939
    and the local and remote (ip, port) information from a string
940
    containing the output of the `drbdsetup show` command as returned
941
    by _GetShowData.
942

943
    """
944
    data = {}
945
    if not out:
946
      return data
947

    
948
    bnf = cls._GetShowParser()
949
    # run pyparse
950

    
951
    try:
952
      results = bnf.parseString(out)
953
    except pyp.ParseException, err:
954
      _ThrowError("Can't parse drbdsetup show output: %s", str(err))
955

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

    
974
  def _MatchesLocal(self, info):
975
    """Test if our local config matches with an existing device.
976

977
    The parameter should be as returned from `_GetDevInfo()`. This
978
    method tests if our local backing device is the same as the one in
979
    the info parameter, in effect testing if we look like the given
980
    device.
981

982
    """
983
    if self._children:
984
      backend, meta = self._children
985
    else:
986
      backend = meta = None
987

    
988
    if backend is not None:
989
      retval = ("local_dev" in info and info["local_dev"] == backend.dev_path)
990
    else:
991
      retval = ("local_dev" not in info)
992

    
993
    if meta is not None:
994
      retval = retval and ("meta_dev" in info and
995
                           info["meta_dev"] == meta.dev_path)
996
      retval = retval and ("meta_index" in info and
997
                           info["meta_index"] == 0)
998
    else:
999
      retval = retval and ("meta_dev" not in info and
1000
                           "meta_index" not in info)
1001
    return retval
1002

    
1003
  def _MatchesNet(self, info):
1004
    """Test if our network config matches with an existing device.
1005

1006
    The parameter should be as returned from `_GetDevInfo()`. This
1007
    method tests if our network configuration is the same as the one
1008
    in the info parameter, in effect testing if we look like the given
1009
    device.
1010

1011
    """
1012
    if (((self._lhost is None and not ("local_addr" in info)) and
1013
         (self._rhost is None and not ("remote_addr" in info)))):
1014
      return True
1015

    
1016
    if self._lhost is None:
1017
      return False
1018

    
1019
    if not ("local_addr" in info and
1020
            "remote_addr" in info):
1021
      return False
1022

    
1023
    retval = (info["local_addr"] == (self._lhost, self._lport))
1024
    retval = (retval and
1025
              info["remote_addr"] == (self._rhost, self._rport))
1026
    return retval
1027

    
1028
  @classmethod
1029
  def _AssembleLocal(cls, minor, backend, meta):
1030
    """Configure the local part of a DRBD device.
1031

1032
    """
1033
    args = ["drbdsetup", cls._DevPath(minor), "disk",
1034
            backend, meta, "0", "-e", "detach", "--create-device"]
1035
    result = utils.RunCmd(args)
1036
    if result.failed:
1037
      _ThrowError("drbd%d: can't attach local disk: %s", minor, result.output)
1038

    
1039
  @classmethod
1040
  def _AssembleNet(cls, minor, net_info, protocol,
1041
                   dual_pri=False, hmac=None, secret=None):
1042
    """Configure the network part of the device.
1043

1044
    """
1045
    lhost, lport, rhost, rport = net_info
1046
    if None in net_info:
1047
      # we don't want network connection and actually want to make
1048
      # sure its shutdown
1049
      cls._ShutdownNet(minor)
1050
      return
1051

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

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

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

    
1091
  def AddChildren(self, devices):
1092
    """Add a disk to the DRBD device.
1093

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

    
1111
    self._AssembleLocal(self.minor, backend.dev_path, meta.dev_path)
1112
    self._children = devices
1113

    
1114
  def RemoveChildren(self, devices):
1115
    """Detach the drbd device from local storage.
1116

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

    
1138
    self._ShutdownLocal(self.minor)
1139
    self._children = []
1140

    
1141
  @classmethod
1142
  def _SetMinorSyncSpeed(cls, minor, kbytes):
1143
    """Set the speed of the DRBD syncer.
1144

1145
    This is the low-level implementation.
1146

1147
    @type minor: int
1148
    @param minor: the drbd minor whose settings we change
1149
    @type kbytes: int
1150
    @param kbytes: the speed in kbytes/second
1151
    @rtype: boolean
1152
    @return: the success of the operation
1153

1154
    """
1155
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "syncer",
1156
                           "-r", "%d" % kbytes, "--create-device"])
1157
    if result.failed:
1158
      logging.error("Can't change syncer rate: %s - %s",
1159
                    result.fail_reason, result.output)
1160
    return not result.failed
1161

    
1162
  def SetSyncSpeed(self, kbytes):
1163
    """Set the speed of the DRBD syncer.
1164

1165
    @type kbytes: int
1166
    @param kbytes: the speed in kbytes/second
1167
    @rtype: boolean
1168
    @return: the success of the operation
1169

1170
    """
1171
    if self.minor is None:
1172
      logging.info("Not attached during SetSyncSpeed")
1173
      return False
1174
    children_result = super(DRBD8, self).SetSyncSpeed(kbytes)
1175
    return self._SetMinorSyncSpeed(self.minor, kbytes) and children_result
1176

    
1177
  def GetProcStatus(self):
1178
    """Return device data from /proc.
1179

1180
    """
1181
    if self.minor is None:
1182
      _ThrowError("drbd%d: GetStats() called while not attached", self._aminor)
1183
    proc_info = self._MassageProcData(self._GetProcData())
1184
    if self.minor not in proc_info:
1185
      _ThrowError("drbd%d: can't find myself in /proc", self.minor)
1186
    return DRBD8Status(proc_info[self.minor])
1187

    
1188
  def GetSyncStatus(self):
1189
    """Returns the sync status of the device.
1190

1191

1192
    If sync_percent is None, it means all is ok
1193
    If estimated_time is None, it means we can't esimate
1194
    the time needed, otherwise it's the time left in seconds.
1195

1196

1197
    We set the is_degraded parameter to True on two conditions:
1198
    network not connected or local disk missing.
1199

1200
    We compute the ldisk parameter based on wheter we have a local
1201
    disk or not.
1202

1203
    @rtype: tuple
1204
    @return: (sync_percent, estimated_time, is_degraded, ldisk)
1205

1206
    """
1207
    if self.minor is None and not self.Attach():
1208
      _ThrowError("drbd%d: can't Attach() in GetSyncStatus", self._aminor)
1209
    stats = self.GetProcStatus()
1210
    ldisk = not stats.is_disk_uptodate
1211
    is_degraded = not stats.is_connected
1212
    return stats.sync_percent, stats.est_time, is_degraded or ldisk, ldisk
1213

    
1214
  def Open(self, force=False):
1215
    """Make the local state primary.
1216

1217
    If the 'force' parameter is given, the '-o' option is passed to
1218
    drbdsetup. Since this is a potentially dangerous operation, the
1219
    force flag should be only given after creation, when it actually
1220
    is mandatory.
1221

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

    
1234
  def Close(self):
1235
    """Make the local state secondary.
1236

1237
    This will, of course, fail if the device is in use.
1238

1239
    """
1240
    if self.minor is None and not self.Attach():
1241
      _ThrowError("drbd%d: can't Attach() in Close()", self._aminor)
1242
    result = utils.RunCmd(["drbdsetup", self.dev_path, "secondary"])
1243
    if result.failed:
1244
      _ThrowError("drbd%d: can't switch drbd device to secondary: %s",
1245
                  self.minor, result.output)
1246

    
1247
  def DisconnectNet(self):
1248
    """Removes network configuration.
1249

1250
    This method shutdowns the network side of the device.
1251

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

1260
    """
1261
    if self.minor is None:
1262
      _ThrowError("drbd%d: disk not attached in re-attach net", self._aminor)
1263

    
1264
    if None in (self._lhost, self._lport, self._rhost, self._rport):
1265
      _ThrowError("drbd%d: DRBD disk missing network info in"
1266
                  " DisconnectNet()", self.minor)
1267

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

    
1283
    if not status.is_standalone:
1284
      if ever_disconnected:
1285
        msg = ("drbd%d: device did not react to the"
1286
               " 'disconnect' command in a timely manner")
1287
      else:
1288
        msg = "drbd%d: can't shutdown network, even after multiple retries"
1289
      _ThrowError(msg, self.minor)
1290

    
1291
    reconfig_time = time.time() - timeout_limit + self._NET_RECONFIG_TIMEOUT
1292
    if reconfig_time > 15: # hardcoded alert limit
1293
      logging.info("drbd%d: DisconnectNet: detach took %.3f seconds",
1294
                   self.minor, reconfig_time)
1295

    
1296
  def AttachNet(self, multimaster):
1297
    """Reconnects the network.
1298

1299
    This method connects the network side of the device with a
1300
    specified multi-master flag. The device needs to be 'Standalone'
1301
    but have valid network configuration data.
1302

1303
    Args:
1304
      - multimaster: init the network in dual-primary mode
1305

1306
    """
1307
    if self.minor is None:
1308
      _ThrowError("drbd%d: device not attached in AttachNet", self._aminor)
1309

    
1310
    if None in (self._lhost, self._lport, self._rhost, self._rport):
1311
      _ThrowError("drbd%d: missing network info in AttachNet()", self.minor)
1312

    
1313
    status = self.GetProcStatus()
1314

    
1315
    if not status.is_standalone:
1316
      _ThrowError("drbd%d: device is not standalone in AttachNet", self.minor)
1317

    
1318
    self._AssembleNet(self.minor,
1319
                      (self._lhost, self._lport, self._rhost, self._rport),
1320
                      constants.DRBD_NET_PROTOCOL, dual_pri=multimaster,
1321
                      hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1322

    
1323
  def Attach(self):
1324
    """Check if our minor is configured.
1325

1326
    This doesn't do any device configurations - it only checks if the
1327
    minor is in a state different from Unconfigured.
1328

1329
    Note that this function will not change the state of the system in
1330
    any way (except in case of side-effects caused by reading from
1331
    /proc).
1332

1333
    """
1334
    used_devs = self.GetUsedDevs()
1335
    if self._aminor in used_devs:
1336
      minor = self._aminor
1337
    else:
1338
      minor = None
1339

    
1340
    self._SetFromMinor(minor)
1341
    return minor is not None
1342

    
1343
  def Assemble(self):
1344
    """Assemble the drbd.
1345

1346
    Method:
1347
      - if we have a configured device, we try to ensure that it matches
1348
        our config
1349
      - if not, we create it from zero
1350

1351
    """
1352
    super(DRBD8, self).Assemble()
1353

    
1354
    self.Attach()
1355
    if self.minor is None:
1356
      # local device completely unconfigured
1357
      self._FastAssemble()
1358
    else:
1359
      # we have to recheck the local and network status and try to fix
1360
      # the device
1361
      self._SlowAssemble()
1362

    
1363
  def _SlowAssemble(self):
1364
    """Assembles the DRBD device from a (partially) configured device.
1365

1366
    In case of partially attached (local device matches but no network
1367
    setup), we perform the network attach. If successful, we re-test
1368
    the attach if can return success.
1369

1370
    """
1371
    net_data = (self._lhost, self._lport, self._rhost, self._rport)
1372
    for minor in (self._aminor,):
1373
      info = self._GetDevInfo(self._GetShowData(minor))
1374
      match_l = self._MatchesLocal(info)
1375
      match_r = self._MatchesNet(info)
1376

    
1377
      if match_l and match_r:
1378
        # everything matches
1379
        break
1380

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

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

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

    
1428
    else:
1429
      minor = None
1430

    
1431
    self._SetFromMinor(minor)
1432
    if minor is None:
1433
      _ThrowError("drbd%d: cannot activate, unknown or unhandled reason",
1434
                  self._aminor)
1435

    
1436
  def _FastAssemble(self):
1437
    """Assemble the drbd device from zero.
1438

1439
    This is run when in Assemble we detect our minor is unused.
1440

1441
    """
1442
    minor = self._aminor
1443
    if self._children and self._children[0] and self._children[1]:
1444
      self._AssembleLocal(minor, self._children[0].dev_path,
1445
                          self._children[1].dev_path)
1446
    if self._lhost and self._lport and self._rhost and self._rport:
1447
      self._AssembleNet(minor,
1448
                        (self._lhost, self._lport, self._rhost, self._rport),
1449
                        constants.DRBD_NET_PROTOCOL,
1450
                        hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1451
    self._SetFromMinor(minor)
1452

    
1453
  @classmethod
1454
  def _ShutdownLocal(cls, minor):
1455
    """Detach from the local device.
1456

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

1460
    """
1461
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "detach"])
1462
    if result.failed:
1463
      _ThrowError("drbd%d: can't detach local disk: %s", minor, result.output)
1464

    
1465
  @classmethod
1466
  def _ShutdownNet(cls, minor):
1467
    """Disconnect from the remote peer.
1468

1469
    This fails if we don't have a local device.
1470

1471
    """
1472
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disconnect"])
1473
    if result.failed:
1474
      _ThrowError("drbd%d: can't shutdown network: %s", minor, result.output)
1475

    
1476
  @classmethod
1477
  def _ShutdownAll(cls, minor):
1478
    """Deactivate the device.
1479

1480
    This will, of course, fail if the device is in use.
1481

1482
    """
1483
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "down"])
1484
    if result.failed:
1485
      _ThrowError("drbd%d: can't shutdown drbd device: %s",
1486
                  minor, result.output)
1487

    
1488
  def Shutdown(self):
1489
    """Shutdown the DRBD device.
1490

1491
    """
1492
    if self.minor is None and not self.Attach():
1493
      logging.info("drbd%d: not attached during Shutdown()", self._aminor)
1494
      return
1495
    minor = self.minor
1496
    self.minor = None
1497
    self.dev_path = None
1498
    self._ShutdownAll(minor)
1499

    
1500
  def Remove(self):
1501
    """Stub remove for DRBD devices.
1502

1503
    """
1504
    self.Shutdown()
1505

    
1506
  @classmethod
1507
  def Create(cls, unique_id, children, size):
1508
    """Create a new DRBD8 device.
1509

1510
    Since DRBD devices are not created per se, just assembled, this
1511
    function only initializes the metadata.
1512

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

    
1535
  def Grow(self, amount):
1536
    """Resize the DRBD device and its backing storage.
1537

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

    
1548

    
1549
class FileStorage(BlockDev):
1550
  """File device.
1551

1552
  This class represents the a file storage backend device.
1553

1554
  The unique_id for the file device is a (file_driver, file_path) tuple.
1555

1556
  """
1557
  def __init__(self, unique_id, children):
1558
    """Initalizes a file device backend.
1559

1560
    """
1561
    if children:
1562
      raise errors.BlockDeviceError("Invalid setup for file device")
1563
    super(FileStorage, self).__init__(unique_id, children)
1564
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1565
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1566
    self.driver = unique_id[0]
1567
    self.dev_path = unique_id[1]
1568
    self.Attach()
1569

    
1570
  def Assemble(self):
1571
    """Assemble the device.
1572

1573
    Checks whether the file device exists, raises BlockDeviceError otherwise.
1574

1575
    """
1576
    if not os.path.exists(self.dev_path):
1577
      _ThrowError("File device '%s' does not exist" % self.dev_path)
1578

    
1579
  def Shutdown(self):
1580
    """Shutdown the device.
1581

1582
    This is a no-op for the file type, as we don't deacivate
1583
    the file on shutdown.
1584

1585
    """
1586
    pass
1587

    
1588
  def Open(self, force=False):
1589
    """Make the device ready for I/O.
1590

1591
    This is a no-op for the file type.
1592

1593
    """
1594
    pass
1595

    
1596
  def Close(self):
1597
    """Notifies that the device will no longer be used for I/O.
1598

1599
    This is a no-op for the file type.
1600

1601
    """
1602
    pass
1603

    
1604
  def Remove(self):
1605
    """Remove the file backing the block device.
1606

1607
    @rtype: boolean
1608
    @return: True if the removal was successful
1609

1610
    """
1611
    try:
1612
      os.remove(self.dev_path)
1613
    except OSError, err:
1614
      if err.errno != errno.ENOENT:
1615
        _ThrowError("Can't remove file '%s': %s", self.dev_path, err)
1616

    
1617
  def Attach(self):
1618
    """Attach to an existing file.
1619

1620
    Check if this file already exists.
1621

1622
    @rtype: boolean
1623
    @return: True if file exists
1624

1625
    """
1626
    self.attached = os.path.exists(self.dev_path)
1627
    return self.attached
1628

    
1629
  @classmethod
1630
  def Create(cls, unique_id, children, size):
1631
    """Create a new file.
1632

1633
    @param size: the size of file in MiB
1634

1635
    @rtype: L{bdev.FileStorage}
1636
    @return: an instance of FileStorage
1637

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

    
1651
    return FileStorage(unique_id, children)
1652

    
1653

    
1654
DEV_MAP = {
1655
  constants.LD_LV: LogicalVolume,
1656
  constants.LD_DRBD8: DRBD8,
1657
  constants.LD_FILE: FileStorage,
1658
  }
1659

    
1660

    
1661
def FindDevice(dev_type, unique_id, children):
1662
  """Search for an existing, assembled device.
1663

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

1667
  """
1668
  if dev_type not in DEV_MAP:
1669
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1670
  device = DEV_MAP[dev_type](unique_id, children)
1671
  if not device.attached:
1672
    return None
1673
  return device
1674

    
1675

    
1676
def Assemble(dev_type, unique_id, children):
1677
  """Try to attach or assemble an existing device.
1678

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

1682
  """
1683
  if dev_type not in DEV_MAP:
1684
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1685
  device = DEV_MAP[dev_type](unique_id, children)
1686
  device.Assemble()
1687
  return device
1688

    
1689

    
1690
def Create(dev_type, unique_id, children, size):
1691
  """Create a device.
1692

1693
  """
1694
  if dev_type not in DEV_MAP:
1695
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1696
  device = DEV_MAP[dev_type].Create(unique_id, children, size)
1697
  return device