Statistics
| Branch: | Tag: | Revision:

root / lib / bdev.py @ f069addf

History | View | Annotate | Download (53.5 kB)

1
#
2
#
3

    
4
# Copyright (C) 2006, 2007 Google Inc.
5
#
6
# This program is free software; you can redistribute it and/or modify
7
# it under the terms of the GNU General Public License as published by
8
# the Free Software Foundation; either version 2 of the License, or
9
# (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful, but
12
# WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14
# General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19
# 02110-1301, USA.
20

    
21

    
22
"""Block device abstraction"""
23

    
24
import re
25
import time
26
import errno
27
import pyparsing as pyp
28
import os
29
import logging
30

    
31
from ganeti import utils
32
from ganeti import errors
33
from ganeti import constants
34

    
35

    
36
def _IgnoreError(fn, *args, **kwargs):
37
  """Executes the given function, ignoring BlockDeviceErrors.
38

39
  This is used in order to simplify the execution of cleanup or
40
  rollback functions.
41

42
  @rtype: boolean
43
  @return: True when fn didn't raise an exception, False otherwise
44

45
  """
46
  try:
47
    fn(*args, **kwargs)
48
    return True
49
  except errors.BlockDeviceError, err:
50
    logging.warning("Caught BlockDeviceError but ignoring: %s" % str(err))
51
    return False
52

    
53

    
54
def _ThrowError(msg, *args):
55
  """Log an error to the node daemon and the raise an exception.
56

57
  @type msg: string
58
  @param msg: the text of the exception
59
  @raise errors.BlockDeviceError
60

61
  """
62
  if args:
63
    msg = msg % args
64
  logging.error(msg)
65
  raise errors.BlockDeviceError(msg)
66

    
67

    
68
class BlockDev(object):
69
  """Block device abstract class.
70

71
  A block device can be in the following states:
72
    - not existing on the system, and by `Create()` it goes into:
73
    - existing but not setup/not active, and by `Assemble()` goes into:
74
    - active read-write and by `Open()` it goes into
75
    - online (=used, or ready for use)
76

77
  A device can also be online but read-only, however we are not using
78
  the readonly state (LV has it, if needed in the future) and we are
79
  usually looking at this like at a stack, so it's easier to
80
  conceptualise the transition from not-existing to online and back
81
  like a linear one.
82

83
  The many different states of the device are due to the fact that we
84
  need to cover many device types:
85
    - logical volumes are created, lvchange -a y $lv, and used
86
    - drbd devices are attached to a local disk/remote peer and made primary
87

88
  A block device is identified by three items:
89
    - the /dev path of the device (dynamic)
90
    - a unique ID of the device (static)
91
    - it's major/minor pair (dynamic)
92

93
  Not all devices implement both the first two as distinct items. LVM
94
  logical volumes have their unique ID (the pair volume group, logical
95
  volume name) in a 1-to-1 relation to the dev path. For DRBD devices,
96
  the /dev path is again dynamic and the unique id is the pair (host1,
97
  dev1), (host2, dev2).
98

99
  You can get to a device in two ways:
100
    - creating the (real) device, which returns you
101
      an attached instance (lvcreate)
102
    - attaching of a python instance to an existing (real) device
103

104
  The second point, the attachement to a device, is different
105
  depending on whether the device is assembled or not. At init() time,
106
  we search for a device with the same unique_id as us. If found,
107
  good. It also means that the device is already assembled. If not,
108
  after assembly we'll have our correct major/minor.
109

110
  """
111
  def __init__(self, unique_id, children, size):
112
    self._children = children
113
    self.dev_path = None
114
    self.unique_id = unique_id
115
    self.major = None
116
    self.minor = None
117
    self.attached = False
118
    self.size = size
119

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

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

131
    """
132
    pass
133

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

137
    """
138
    raise NotImplementedError
139

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

143
    """
144
    raise NotImplementedError
145

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

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

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

157
    """
158
    raise NotImplementedError
159

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

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

167
    """
168
    raise NotImplementedError
169

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

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

175
    """
176
    raise NotImplementedError
177

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

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

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

187
    """
188
    raise NotImplementedError
189

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

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

197
    """
198
    raise NotImplementedError
199

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

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

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

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

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

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

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

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

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

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

234
    """
235
    return None, None, False, False
236

    
237

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

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

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

    
262

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

266
    Only supported for some device types.
267

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

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

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

277
    """
278
    raise NotImplementedError
279

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

    
285

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

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

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

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

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

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

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

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

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

339
    @param vg_name: the volume group name
340

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

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

    
364
    return data
365

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

453
    """
454
    pass
455

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

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

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

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

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

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

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

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

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

486
    """
487
    pass
488

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

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

494
    """
495
    pass
496

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

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

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

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

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

    
523
    return snap_name
524

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

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

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

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

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

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

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

    
559

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

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

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

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

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

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

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

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

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

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

    
620

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

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

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

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

    
636
  _STATUS_FILE = "/proc/drbd"
637

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

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

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

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

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

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

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

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

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

    
715
    return retval
716

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

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

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

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

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

    
743
    return used_devs
744

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

748
    This sets our minor variable and our dev_path.
749

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

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

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

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

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

784
    This is not supported for drbd devices.
785

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

    
789

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

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

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

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

    
806
  # timeout constants
807
  _NET_RECONFIG_TIMEOUT = 60
808

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
924
    cls._PARSE_SHOW = bnf
925

    
926
    return bnf
927

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1100
  def AddChildren(self, devices):
1101
    """Add a disk to the DRBD device.
1102

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

    
1120
    self._AssembleLocal(self.minor, backend.dev_path, meta.dev_path, self.size)
1121
    self._children = devices
1122

    
1123
  def RemoveChildren(self, devices):
1124
    """Detach the drbd device from local storage.
1125

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

    
1147
    self._ShutdownLocal(self.minor)
1148
    self._children = []
1149

    
1150
  @classmethod
1151
  def _SetMinorSyncSpeed(cls, minor, kbytes):
1152
    """Set the speed of the DRBD syncer.
1153

1154
    This is the low-level implementation.
1155

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

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

    
1171
  def SetSyncSpeed(self, kbytes):
1172
    """Set the speed of the DRBD syncer.
1173

1174
    @type kbytes: int
1175
    @param kbytes: the speed in kbytes/second
1176
    @rtype: boolean
1177
    @return: the success of the operation
1178

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

    
1186
  def GetProcStatus(self):
1187
    """Return device data from /proc.
1188

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

    
1197
  def GetSyncStatus(self):
1198
    """Returns the sync status of the device.
1199

1200

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

1205

1206
    We set the is_degraded parameter to True on two conditions:
1207
    network not connected or local disk missing.
1208

1209
    We compute the ldisk parameter based on wheter we have a local
1210
    disk or not.
1211

1212
    @rtype: tuple
1213
    @return: (sync_percent, estimated_time, is_degraded, ldisk)
1214

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

    
1223
  def Open(self, force=False):
1224
    """Make the local state primary.
1225

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

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

    
1243
  def Close(self):
1244
    """Make the local state secondary.
1245

1246
    This will, of course, fail if the device is in use.
1247

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

    
1256
  def DisconnectNet(self):
1257
    """Removes network configuration.
1258

1259
    This method shutdowns the network side of the device.
1260

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

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

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

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

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

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

    
1305
  def AttachNet(self, multimaster):
1306
    """Reconnects the network.
1307

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

1312
    Args:
1313
      - multimaster: init the network in dual-primary mode
1314

1315
    """
1316
    if self.minor is None:
1317
      _ThrowError("drbd%d: device not attached in AttachNet", self._aminor)
1318

    
1319
    if None in (self._lhost, self._lport, self._rhost, self._rport):
1320
      _ThrowError("drbd%d: missing network info in AttachNet()", self.minor)
1321

    
1322
    status = self.GetProcStatus()
1323

    
1324
    if not status.is_standalone:
1325
      _ThrowError("drbd%d: device is not standalone in AttachNet", self.minor)
1326

    
1327
    self._AssembleNet(self.minor,
1328
                      (self._lhost, self._lport, self._rhost, self._rport),
1329
                      constants.DRBD_NET_PROTOCOL, dual_pri=multimaster,
1330
                      hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1331

    
1332
  def Attach(self):
1333
    """Check if our minor is configured.
1334

1335
    This doesn't do any device configurations - it only checks if the
1336
    minor is in a state different from Unconfigured.
1337

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

1342
    """
1343
    used_devs = self.GetUsedDevs()
1344
    if self._aminor in used_devs:
1345
      minor = self._aminor
1346
    else:
1347
      minor = None
1348

    
1349
    self._SetFromMinor(minor)
1350
    return minor is not None
1351

    
1352
  def Assemble(self):
1353
    """Assemble the drbd.
1354

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

1360
    """
1361
    super(DRBD8, self).Assemble()
1362

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

    
1372
  def _SlowAssemble(self):
1373
    """Assembles the DRBD device from a (partially) configured device.
1374

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

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

    
1386
      if match_l and match_r:
1387
        # everything matches
1388
        break
1389

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

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

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

    
1437
    else:
1438
      minor = None
1439

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

    
1445
  def _FastAssemble(self):
1446
    """Assemble the drbd device from zero.
1447

1448
    This is run when in Assemble we detect our minor is unused.
1449

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

    
1462
  @classmethod
1463
  def _ShutdownLocal(cls, minor):
1464
    """Detach from the local device.
1465

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

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

    
1474
  @classmethod
1475
  def _ShutdownNet(cls, minor):
1476
    """Disconnect from the remote peer.
1477

1478
    This fails if we don't have a local device.
1479

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

    
1485
  @classmethod
1486
  def _ShutdownAll(cls, minor):
1487
    """Deactivate the device.
1488

1489
    This will, of course, fail if the device is in use.
1490

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

    
1497
  def Shutdown(self):
1498
    """Shutdown the DRBD device.
1499

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

    
1509
  def Remove(self):
1510
    """Stub remove for DRBD devices.
1511

1512
    """
1513
    self.Shutdown()
1514

    
1515
  @classmethod
1516
  def Create(cls, unique_id, children, size):
1517
    """Create a new DRBD8 device.
1518

1519
    Since DRBD devices are not created per se, just assembled, this
1520
    function only initializes the metadata.
1521

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

    
1544
  def Grow(self, amount):
1545
    """Resize the DRBD device and its backing storage.
1546

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

    
1557

    
1558
class FileStorage(BlockDev):
1559
  """File device.
1560

1561
  This class represents the a file storage backend device.
1562

1563
  The unique_id for the file device is a (file_driver, file_path) tuple.
1564

1565
  """
1566
  def __init__(self, unique_id, children, size):
1567
    """Initalizes a file device backend.
1568

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

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

1582
    Checks whether the file device exists, raises BlockDeviceError otherwise.
1583

1584
    """
1585
    if not os.path.exists(self.dev_path):
1586
      _ThrowError("File device '%s' does not exist" % self.dev_path)
1587

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

1591
    This is a no-op for the file type, as we don't deacivate
1592
    the file on shutdown.
1593

1594
    """
1595
    pass
1596

    
1597
  def Open(self, force=False):
1598
    """Make the device ready for I/O.
1599

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

1602
    """
1603
    pass
1604

    
1605
  def Close(self):
1606
    """Notifies that the device will no longer be used for I/O.
1607

1608
    This is a no-op for the file type.
1609

1610
    """
1611
    pass
1612

    
1613
  def Remove(self):
1614
    """Remove the file backing the block device.
1615

1616
    @rtype: boolean
1617
    @return: True if the removal was successful
1618

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

    
1626
  def Attach(self):
1627
    """Attach to an existing file.
1628

1629
    Check if this file already exists.
1630

1631
    @rtype: boolean
1632
    @return: True if file exists
1633

1634
    """
1635
    self.attached = os.path.exists(self.dev_path)
1636
    return self.attached
1637

    
1638
  @classmethod
1639
  def Create(cls, unique_id, children, size):
1640
    """Create a new file.
1641

1642
    @param size: the size of file in MiB
1643

1644
    @rtype: L{bdev.FileStorage}
1645
    @return: an instance of FileStorage
1646

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

    
1660
    return FileStorage(unique_id, children, size)
1661

    
1662

    
1663
DEV_MAP = {
1664
  constants.LD_LV: LogicalVolume,
1665
  constants.LD_DRBD8: DRBD8,
1666
  constants.LD_FILE: FileStorage,
1667
  }
1668

    
1669

    
1670
def FindDevice(dev_type, unique_id, children, size):
1671
  """Search for an existing, assembled device.
1672

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

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

    
1684

    
1685
def Assemble(dev_type, unique_id, children, size):
1686
  """Try to attach or assemble an existing device.
1687

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

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

    
1698

    
1699
def Create(dev_type, unique_id, children, size):
1700
  """Create a device.
1701

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