Statistics
| Branch: | Tag: | Revision:

root / lib / bdev.py @ 59b4eeef

History | View | Annotate | Download (53.3 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|ro):([^/]+)/(\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_type = (pyp.Optional(pyp.Literal("ipv4")).suppress() +
900
                 pyp.Optional(pyp.Literal("ipv6")).suppress())
901
    addr_port = (addr_type + pyp.Word(pyp.nums + '.') +
902
                 pyp.Literal(':').suppress() + number)
903
    # meta device, extended syntax
904
    meta_value = ((value ^ quoted) + pyp.Literal('[').suppress() +
905
                  number + pyp.Word(']').suppress())
906
    # device name, extended syntax
907
    device_value = pyp.Literal("minor").suppress() + number
908

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

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

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

    
923
    cls._PARSE_SHOW = bnf
924

    
925
    return bnf
926

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1150
    This is the low-level implementation.
1151

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

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

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

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

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

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

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

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

1196

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

1201

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1318
    status = self.GetProcStatus()
1319

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1433
    else:
1434
      minor = None
1435

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1508
    """
1509
    self.Shutdown()
1510

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

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

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

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

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

    
1553

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

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

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

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

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

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

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

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

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

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

1590
    """
1591
    pass
1592

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

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

1598
    """
1599
    pass
1600

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

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

1606
    """
1607
    pass
1608

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

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

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

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

1625
    Check if this file already exists.
1626

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

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

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

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

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

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

    
1656
    return FileStorage(unique_id, children)
1657

    
1658

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

    
1665

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

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

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

    
1680

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

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

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

    
1694

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

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