Statistics
| Branch: | Tag: | Revision:

root / lib / bdev.py @ 04864530

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

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

    
35

    
36
class BlockDev(object):
37
  """Block device abstract class.
38

39
  A block device can be in the following states:
40
    - not existing on the system, and by `Create()` it goes into:
41
    - existing but not setup/not active, and by `Assemble()` goes into:
42
    - active read-write and by `Open()` it goes into
43
    - online (=used, or ready for use)
44

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

51
  The many different states of the device are due to the fact that we
52
  need to cover many device types:
53
    - logical volumes are created, lvchange -a y $lv, and used
54
    - drbd devices are attached to a local disk/remote peer and made primary
55

56
  A block device is identified by three items:
57
    - the /dev path of the device (dynamic)
58
    - a unique ID of the device (static)
59
    - it's major/minor pair (dynamic)
60

61
  Not all devices implement both the first two as distinct items. LVM
62
  logical volumes have their unique ID (the pair volume group, logical
63
  volume name) in a 1-to-1 relation to the dev path. For DRBD devices,
64
  the /dev path is again dynamic and the unique id is the pair (host1,
65
  dev1), (host2, dev2).
66

67
  You can get to a device in two ways:
68
    - creating the (real) device, which returns you
69
      an attached instance (lvcreate)
70
    - attaching of a python instance to an existing (real) device
71

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

78
  """
79
  def __init__(self, unique_id, children):
80
    self._children = children
81
    self.dev_path = None
82
    self.unique_id = unique_id
83
    self.major = None
84
    self.minor = None
85

    
86
  def Assemble(self):
87
    """Assemble the device from its components.
88

89
    If this is a plain block device (e.g. LVM) than assemble does
90
    nothing, as the LVM has no children and we don't put logical
91
    volumes offline.
92

93
    One guarantee is that after the device has been assembled, it
94
    knows its major/minor numbers. This allows other devices (usually
95
    parents) to probe correctly for their children.
96

97
    """
98
    status = True
99
    for child in self._children:
100
      if not isinstance(child, BlockDev):
101
        raise TypeError("Invalid child passed of type '%s'" % type(child))
102
      if not status:
103
        break
104
      status = status and child.Assemble()
105
      if not status:
106
        break
107

    
108
      try:
109
        child.Open()
110
      except errors.BlockDeviceError:
111
        for child in self._children:
112
          child.Shutdown()
113
        raise
114

    
115
    if not status:
116
      for child in self._children:
117
        child.Shutdown()
118
    return status
119

    
120
  def Attach(self):
121
    """Find a device which matches our config and attach to it.
122

123
    """
124
    raise NotImplementedError
125

    
126
  def Close(self):
127
    """Notifies that the device will no longer be used for I/O.
128

129
    """
130
    raise NotImplementedError
131

    
132
  @classmethod
133
  def Create(cls, unique_id, children, size):
134
    """Create the device.
135

136
    If the device cannot be created, it will return None
137
    instead. Error messages go to the logging system.
138

139
    Note that for some devices, the unique_id is used, and for other,
140
    the children. The idea is that these two, taken together, are
141
    enough for both creation and assembly (later).
142

143
    """
144
    raise NotImplementedError
145

    
146
  def Remove(self):
147
    """Remove this device.
148

149
    This makes sense only for some of the device types: LV and file
150
    storeage. Also note that if the device can't attach, the removal
151
    can't be completed.
152

153
    """
154
    raise NotImplementedError
155

    
156
  def Rename(self, new_id):
157
    """Rename this device.
158

159
    This may or may not make sense for a given device type.
160

161
    """
162
    raise NotImplementedError
163

    
164
  def Open(self, force=False):
165
    """Make the device ready for use.
166

167
    This makes the device ready for I/O. For now, just the DRBD
168
    devices need this.
169

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

173
    """
174
    raise NotImplementedError
175

    
176
  def Shutdown(self):
177
    """Shut down the device, freeing its children.
178

179
    This undoes the `Assemble()` work, except for the child
180
    assembling; as such, the children on the device are still
181
    assembled after this call.
182

183
    """
184
    raise NotImplementedError
185

    
186
  def SetSyncSpeed(self, speed):
187
    """Adjust the sync speed of the mirror.
188

189
    In case this is not a mirroring device, this is no-op.
190

191
    """
192
    result = True
193
    if self._children:
194
      for child in self._children:
195
        result = result and child.SetSyncSpeed(speed)
196
    return result
197

    
198
  def GetSyncStatus(self):
199
    """Returns the sync status of the device.
200

201
    If this device is a mirroring device, this function returns the
202
    status of the mirror.
203

204
    Returns:
205
     (sync_percent, estimated_time, is_degraded, ldisk)
206

207
    If sync_percent is None, it means the device is not syncing.
208

209
    If estimated_time is None, it means we can't estimate
210
    the time needed, otherwise it's the time left in seconds.
211

212
    If is_degraded is True, it means the device is missing
213
    redundancy. This is usually a sign that something went wrong in
214
    the device setup, if sync_percent is None.
215

216
    The ldisk parameter represents the degradation of the local
217
    data. This is only valid for some devices, the rest will always
218
    return False (not degraded).
219

220
    """
221
    return None, None, False, False
222

    
223

    
224
  def CombinedSyncStatus(self):
225
    """Calculate the mirror status recursively for our children.
226

227
    The return value is the same as for `GetSyncStatus()` except the
228
    minimum percent and maximum time are calculated across our
229
    children.
230

231
    """
232
    min_percent, max_time, is_degraded, ldisk = self.GetSyncStatus()
233
    if self._children:
234
      for child in self._children:
235
        c_percent, c_time, c_degraded, c_ldisk = child.GetSyncStatus()
236
        if min_percent is None:
237
          min_percent = c_percent
238
        elif c_percent is not None:
239
          min_percent = min(min_percent, c_percent)
240
        if max_time is None:
241
          max_time = c_time
242
        elif c_time is not None:
243
          max_time = max(max_time, c_time)
244
        is_degraded = is_degraded or c_degraded
245
        ldisk = ldisk or c_ldisk
246
    return min_percent, max_time, is_degraded, ldisk
247

    
248

    
249
  def SetInfo(self, text):
250
    """Update metadata with info text.
251

252
    Only supported for some device types.
253

254
    """
255
    for child in self._children:
256
      child.SetInfo(text)
257

    
258
  def Grow(self, amount):
259
    """Grow the block device.
260

261
    Arguments:
262
      amount: the amount (in mebibytes) to grow with
263

264
    Returns: None
265

266
    """
267
    raise NotImplementedError
268

    
269
  def __repr__(self):
270
    return ("<%s: unique_id: %s, children: %s, %s:%s, %s>" %
271
            (self.__class__, self.unique_id, self._children,
272
             self.major, self.minor, self.dev_path))
273

    
274

    
275
class LogicalVolume(BlockDev):
276
  """Logical Volume block device.
277

278
  """
279
  def __init__(self, unique_id, children):
280
    """Attaches to a LV device.
281

282
    The unique_id is a tuple (vg_name, lv_name)
283

284
    """
285
    super(LogicalVolume, self).__init__(unique_id, children)
286
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
287
      raise ValueError("Invalid configuration data %s" % str(unique_id))
288
    self._vg_name, self._lv_name = unique_id
289
    self.dev_path = "/dev/%s/%s" % (self._vg_name, self._lv_name)
290
    self._degraded = True
291
    self.major = self.minor = None
292
    self.Attach()
293

    
294
  @classmethod
295
  def Create(cls, unique_id, children, size):
296
    """Create a new logical volume.
297

298
    """
299
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
300
      raise ValueError("Invalid configuration data %s" % str(unique_id))
301
    vg_name, lv_name = unique_id
302
    pvs_info = cls.GetPVInfo(vg_name)
303
    if not pvs_info:
304
      raise errors.BlockDeviceError("Can't compute PV info for vg %s" %
305
                                    vg_name)
306
    pvs_info.sort()
307
    pvs_info.reverse()
308

    
309
    pvlist = [ pv[1] for pv in pvs_info ]
310
    free_size = sum([ pv[0] for pv in pvs_info ])
311

    
312
    # The size constraint should have been checked from the master before
313
    # calling the create function.
314
    if free_size < size:
315
      raise errors.BlockDeviceError("Not enough free space: required %s,"
316
                                    " available %s" % (size, free_size))
317
    result = utils.RunCmd(["lvcreate", "-L%dm" % size, "-n%s" % lv_name,
318
                           vg_name] + pvlist)
319
    if result.failed:
320
      raise errors.BlockDeviceError("%s - %s" % (result.fail_reason,
321
                                                result.output))
322
    return LogicalVolume(unique_id, children)
323

    
324
  @staticmethod
325
  def GetPVInfo(vg_name):
326
    """Get the free space info for PVs in a volume group.
327

328
    Args:
329
      vg_name: the volume group name
330

331
    Returns:
332
      list of (free_space, name) with free_space in mebibytes
333

334
    """
335
    command = ["pvs", "--noheadings", "--nosuffix", "--units=m",
336
               "-opv_name,vg_name,pv_free,pv_attr", "--unbuffered",
337
               "--separator=:"]
338
    result = utils.RunCmd(command)
339
    if result.failed:
340
      logger.Error("Can't get the PV information: %s - %s" %
341
                   (result.fail_reason, result.output))
342
      return None
343
    data = []
344
    for line in result.stdout.splitlines():
345
      fields = line.strip().split(':')
346
      if len(fields) != 4:
347
        logger.Error("Can't parse pvs output: line '%s'" % line)
348
        return None
349
      # skip over pvs from another vg or ones which are not allocatable
350
      if fields[1] != vg_name or fields[3][0] != 'a':
351
        continue
352
      data.append((float(fields[2]), fields[0]))
353

    
354
    return data
355

    
356
  def Remove(self):
357
    """Remove this logical volume.
358

359
    """
360
    if not self.minor and not self.Attach():
361
      # the LV does not exist
362
      return True
363
    result = utils.RunCmd(["lvremove", "-f", "%s/%s" %
364
                           (self._vg_name, self._lv_name)])
365
    if result.failed:
366
      logger.Error("Can't lvremove: %s - %s" %
367
                   (result.fail_reason, result.output))
368

    
369
    return not result.failed
370

    
371
  def Rename(self, new_id):
372
    """Rename this logical volume.
373

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

    
389
  def Attach(self):
390
    """Attach to an existing LV.
391

392
    This method will try to see if an existing and active LV exists
393
    which matches our name. If so, its major/minor will be
394
    recorded.
395

396
    """
397
    result = utils.RunCmd(["lvs", "--noheadings", "--separator=,",
398
                           "-olv_attr,lv_kernel_major,lv_kernel_minor",
399
                           self.dev_path])
400
    if result.failed:
401
      logger.Error("Can't find LV %s: %s, %s" %
402
                   (self.dev_path, result.fail_reason, result.output))
403
      return False
404
    out = result.stdout.strip().rstrip(',')
405
    out = out.split(",")
406
    if len(out) != 3:
407
      logger.Error("Can't parse LVS output, len(%s) != 3" % str(out))
408
      return False
409

    
410
    status, major, minor = out[:3]
411
    if len(status) != 6:
412
      logger.Error("lvs lv_attr is not 6 characters (%s)" % status)
413
      return False
414

    
415
    try:
416
      major = int(major)
417
      minor = int(minor)
418
    except ValueError, err:
419
      logger.Error("lvs major/minor cannot be parsed: %s" % str(err))
420

    
421
    self.major = major
422
    self.minor = minor
423
    self._degraded = status[0] == 'v' # virtual volume, i.e. doesn't backing
424
                                      # storage
425
    return True
426

    
427
  def Assemble(self):
428
    """Assemble the device.
429

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

434
    """
435
    result = utils.RunCmd(["lvchange", "-ay", self.dev_path])
436
    if result.failed:
437
      logger.Error("Can't activate lv %s: %s" % (self.dev_path, result.output))
438
    return not result.failed
439

    
440
  def Shutdown(self):
441
    """Shutdown the device.
442

443
    This is a no-op for the LV device type, as we don't deactivate the
444
    volumes on shutdown.
445

446
    """
447
    return True
448

    
449
  def GetSyncStatus(self):
450
    """Returns the sync status of the device.
451

452
    If this device is a mirroring device, this function returns the
453
    status of the mirror.
454

455
    Returns:
456
     (sync_percent, estimated_time, is_degraded, ldisk)
457

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

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

469
    The status was already read in Attach, so we just return it.
470

471
    """
472
    return None, None, self._degraded, self._degraded
473

    
474
  def Open(self, force=False):
475
    """Make the device ready for I/O.
476

477
    This is a no-op for the LV device type.
478

479
    """
480
    pass
481

    
482
  def Close(self):
483
    """Notifies that the device will no longer be used for I/O.
484

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

487
    """
488
    pass
489

    
490
  def Snapshot(self, size):
491
    """Create a snapshot copy of an lvm block device.
492

493
    """
494
    snap_name = self._lv_name + ".snap"
495

    
496
    # remove existing snapshot if found
497
    snap = LogicalVolume((self._vg_name, snap_name), None)
498
    snap.Remove()
499

    
500
    pvs_info = self.GetPVInfo(self._vg_name)
501
    if not pvs_info:
502
      raise errors.BlockDeviceError("Can't compute PV info for vg %s" %
503
                                    self._vg_name)
504
    pvs_info.sort()
505
    pvs_info.reverse()
506
    free_size, pv_name = pvs_info[0]
507
    if free_size < size:
508
      raise errors.BlockDeviceError("Not enough free space: required %s,"
509
                                    " available %s" % (size, free_size))
510

    
511
    result = utils.RunCmd(["lvcreate", "-L%dm" % size, "-s",
512
                           "-n%s" % snap_name, self.dev_path])
513
    if result.failed:
514
      raise errors.BlockDeviceError("command: %s error: %s - %s" %
515
                                    (result.cmd, result.fail_reason,
516
                                     result.output))
517

    
518
    return snap_name
519

    
520
  def SetInfo(self, text):
521
    """Update metadata with info text.
522

523
    """
524
    BlockDev.SetInfo(self, text)
525

    
526
    # Replace invalid characters
527
    text = re.sub('^[^A-Za-z0-9_+.]', '_', text)
528
    text = re.sub('[^-A-Za-z0-9_+.]', '_', text)
529

    
530
    # Only up to 128 characters are allowed
531
    text = text[:128]
532

    
533
    result = utils.RunCmd(["lvchange", "--addtag", text,
534
                           self.dev_path])
535
    if result.failed:
536
      raise errors.BlockDeviceError("Command: %s error: %s - %s" %
537
                                    (result.cmd, result.fail_reason,
538
                                     result.output))
539
  def Grow(self, amount):
540
    """Grow the logical volume.
541

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

    
555

    
556
class DRBD8Status(object):
557
  """A DRBD status representation class.
558

559
  Note that this doesn't support unconfigured devices (cs:Unconfigured).
560

561
  """
562
  LINE_RE = re.compile(r"\s*[0-9]+:\s*cs:(\S+)\s+st:([^/]+)/(\S+)"
563
                       "\s+ds:([^/]+)/(\S+)\s+.*$")
564
  SYNC_RE = re.compile(r"^.*\ssync'ed:\s*([0-9.]+)%.*"
565
                       "\sfinish: ([0-9]+):([0-9]+):([0-9]+)\s.*$")
566

    
567
  def __init__(self, procline):
568
    m = self.LINE_RE.match(procline)
569
    if not m:
570
      raise errors.BlockDeviceError("Can't parse input data '%s'" % procline)
571
    self.cstatus = m.group(1)
572
    self.lrole = m.group(2)
573
    self.rrole = m.group(3)
574
    self.ldisk = m.group(4)
575
    self.rdisk = m.group(5)
576

    
577
    self.is_standalone = self.cstatus == "StandAlone"
578
    self.is_wfconn = self.cstatus == "WFConnection"
579
    self.is_connected = self.cstatus == "Connected"
580
    self.is_primary = self.lrole == "Primary"
581
    self.is_secondary = self.lrole == "Secondary"
582
    self.peer_primary = self.rrole == "Primary"
583
    self.peer_secondary = self.rrole == "Secondary"
584
    self.both_primary = self.is_primary and self.peer_primary
585
    self.both_secondary = self.is_secondary and self.peer_secondary
586

    
587
    self.is_diskless = self.ldisk == "Diskless"
588
    self.is_disk_uptodate = self.ldisk == "UpToDate"
589

    
590
    m = self.SYNC_RE.match(procline)
591
    if m:
592
      self.sync_percent = float(m.group(1))
593
      hours = int(m.group(2))
594
      minutes = int(m.group(3))
595
      seconds = int(m.group(4))
596
      self.est_time = hours * 3600 + minutes * 60 + seconds
597
    else:
598
      self.sync_percent = None
599
      self.est_time = None
600

    
601
    self.is_sync_target = self.peer_sync_source = self.cstatus == "SyncTarget"
602
    self.peer_sync_target = self.is_sync_source = self.cstatus == "SyncSource"
603
    self.is_resync = self.is_sync_target or self.is_sync_source
604

    
605

    
606
class BaseDRBD(BlockDev):
607
  """Base DRBD class.
608

609
  This class contains a few bits of common functionality between the
610
  0.7 and 8.x versions of DRBD.
611

612
  """
613
  _VERSION_RE = re.compile(r"^version: (\d+)\.(\d+)\.(\d+)"
614
                           r" \(api:(\d+)/proto:(\d+)(?:-(\d+))?\)")
615

    
616
  _DRBD_MAJOR = 147
617
  _ST_UNCONFIGURED = "Unconfigured"
618
  _ST_WFCONNECTION = "WFConnection"
619
  _ST_CONNECTED = "Connected"
620

    
621
  _STATUS_FILE = "/proc/drbd"
622

    
623
  @staticmethod
624
  def _GetProcData(filename=_STATUS_FILE):
625
    """Return data from /proc/drbd.
626

627
    """
628
    stat = open(filename, "r")
629
    try:
630
      data = stat.read().splitlines()
631
    finally:
632
      stat.close()
633
    if not data:
634
      raise errors.BlockDeviceError("Can't read any data from %s" % filename)
635
    return data
636

    
637
  @staticmethod
638
  def _MassageProcData(data):
639
    """Transform the output of _GetProdData into a nicer form.
640

641
    Returns:
642
      a dictionary of minor: joined lines from /proc/drbd for that minor
643

644
    """
645
    lmatch = re.compile("^ *([0-9]+):.*$")
646
    results = {}
647
    old_minor = old_line = None
648
    for line in data:
649
      lresult = lmatch.match(line)
650
      if lresult is not None:
651
        if old_minor is not None:
652
          results[old_minor] = old_line
653
        old_minor = int(lresult.group(1))
654
        old_line = line
655
      else:
656
        if old_minor is not None:
657
          old_line += " " + line.strip()
658
    # add last line
659
    if old_minor is not None:
660
      results[old_minor] = old_line
661
    return results
662

    
663
  @classmethod
664
  def _GetVersion(cls):
665
    """Return the DRBD version.
666

667
    This will return a dict with keys:
668
      k_major,
669
      k_minor,
670
      k_point,
671
      api,
672
      proto,
673
      proto2 (only on drbd > 8.2.X)
674

675
    """
676
    proc_data = cls._GetProcData()
677
    first_line = proc_data[0].strip()
678
    version = cls._VERSION_RE.match(first_line)
679
    if not version:
680
      raise errors.BlockDeviceError("Can't parse DRBD version from '%s'" %
681
                                    first_line)
682

    
683
    values = version.groups()
684
    retval = {'k_major': int(values[0]),
685
              'k_minor': int(values[1]),
686
              'k_point': int(values[2]),
687
              'api': int(values[3]),
688
              'proto': int(values[4]),
689
             }
690
    if values[5] is not None:
691
      retval['proto2'] = values[5]
692

    
693
    return retval
694

    
695
  @staticmethod
696
  def _DevPath(minor):
697
    """Return the path to a drbd device for a given minor.
698

699
    """
700
    return "/dev/drbd%d" % minor
701

    
702
  @classmethod
703
  def _GetUsedDevs(cls):
704
    """Compute the list of used DRBD devices.
705

706
    """
707
    data = cls._GetProcData()
708

    
709
    used_devs = {}
710
    valid_line = re.compile("^ *([0-9]+): cs:([^ ]+).*$")
711
    for line in data:
712
      match = valid_line.match(line)
713
      if not match:
714
        continue
715
      minor = int(match.group(1))
716
      state = match.group(2)
717
      if state == cls._ST_UNCONFIGURED:
718
        continue
719
      used_devs[minor] = state, line
720

    
721
    return used_devs
722

    
723
  def _SetFromMinor(self, minor):
724
    """Set our parameters based on the given minor.
725

726
    This sets our minor variable and our dev_path.
727

728
    """
729
    if minor is None:
730
      self.minor = self.dev_path = None
731
    else:
732
      self.minor = minor
733
      self.dev_path = self._DevPath(minor)
734

    
735
  @staticmethod
736
  def _CheckMetaSize(meta_device):
737
    """Check if the given meta device looks like a valid one.
738

739
    This currently only check the size, which must be around
740
    128MiB.
741

742
    """
743
    result = utils.RunCmd(["blockdev", "--getsize", meta_device])
744
    if result.failed:
745
      logger.Error("Failed to get device size: %s - %s" %
746
                   (result.fail_reason, result.output))
747
      return False
748
    try:
749
      sectors = int(result.stdout)
750
    except ValueError:
751
      logger.Error("Invalid output from blockdev: '%s'" % result.stdout)
752
      return False
753
    bytes = sectors * 512
754
    if bytes < 128 * 1024 * 1024: # less than 128MiB
755
      logger.Error("Meta device too small (%.2fMib)" % (bytes / 1024 / 1024))
756
      return False
757
    if bytes > (128 + 32) * 1024 * 1024: # account for an extra (big) PE on LVM
758
      logger.Error("Meta device too big (%.2fMiB)" % (bytes / 1024 / 1024))
759
      return False
760
    return True
761

    
762
  def Rename(self, new_id):
763
    """Rename a device.
764

765
    This is not supported for drbd devices.
766

767
    """
768
    raise errors.ProgrammerError("Can't rename a drbd device")
769

    
770

    
771
class DRBD8(BaseDRBD):
772
  """DRBD v8.x block device.
773

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

778
  The unique_id for the drbd device is the (local_ip, local_port,
779
  remote_ip, remote_port) tuple, and it must have two children: the
780
  data device and the meta_device. The meta device is checked for
781
  valid size and is zeroed on create.
782

783
  """
784
  _MAX_MINORS = 255
785
  _PARSE_SHOW = None
786

    
787
  def __init__(self, unique_id, children):
788
    if children and children.count(None) > 0:
789
      children = []
790
    super(DRBD8, self).__init__(unique_id, children)
791
    self.major = self._DRBD_MAJOR
792
    version = self._GetVersion()
793
    if version['k_major'] != 8 :
794
      raise errors.BlockDeviceError("Mismatch in DRBD kernel version and"
795
                                    " requested ganeti usage: kernel is"
796
                                    " %s.%s, ganeti wants 8.x" %
797
                                    (version['k_major'], version['k_minor']))
798

    
799
    if len(children) not in (0, 2):
800
      raise ValueError("Invalid configuration data %s" % str(children))
801
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 4:
802
      raise ValueError("Invalid configuration data %s" % str(unique_id))
803
    self._lhost, self._lport, self._rhost, self._rport = unique_id
804
    self.Attach()
805

    
806
  @classmethod
807
  def _InitMeta(cls, minor, dev_path):
808
    """Initialize a meta device.
809

810
    This will not work if the given minor is in use.
811

812
    """
813
    result = utils.RunCmd(["drbdmeta", "--force", cls._DevPath(minor),
814
                           "v08", dev_path, "0", "create-md"])
815
    if result.failed:
816
      raise errors.BlockDeviceError("Can't initialize meta device: %s" %
817
                                    result.output)
818

    
819
  @classmethod
820
  def _FindUnusedMinor(cls):
821
    """Find an unused DRBD device.
822

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

826
    """
827
    data = cls._GetProcData()
828

    
829
    unused_line = re.compile("^ *([0-9]+): cs:Unconfigured$")
830
    used_line = re.compile("^ *([0-9]+): cs:")
831
    highest = None
832
    for line in data:
833
      match = unused_line.match(line)
834
      if match:
835
        return int(match.group(1))
836
      match = used_line.match(line)
837
      if match:
838
        minor = int(match.group(1))
839
        highest = max(highest, minor)
840
    if highest is None: # there are no minors in use at all
841
      return 0
842
    if highest >= cls._MAX_MINORS:
843
      logger.Error("Error: no free drbd minors!")
844
      raise errors.BlockDeviceError("Can't find a free DRBD minor")
845
    return highest + 1
846

    
847
  @classmethod
848
  def _IsValidMeta(cls, meta_device):
849
    """Check if the given meta device looks like a valid one.
850

851
    """
852
    minor = cls._FindUnusedMinor()
853
    minor_path = cls._DevPath(minor)
854
    result = utils.RunCmd(["drbdmeta", minor_path,
855
                           "v08", meta_device, "0",
856
                           "dstate"])
857
    if result.failed:
858
      logger.Error("Invalid meta device %s: %s" % (meta_device, result.output))
859
      return False
860
    return True
861

    
862
  @classmethod
863
  def _GetShowParser(cls):
864
    """Return a parser for `drbd show` output.
865

866
    This will either create or return an already-create parser for the
867
    output of the command `drbd show`.
868

869
    """
870
    if cls._PARSE_SHOW is not None:
871
      return cls._PARSE_SHOW
872

    
873
    # pyparsing setup
874
    lbrace = pyp.Literal("{").suppress()
875
    rbrace = pyp.Literal("}").suppress()
876
    semi = pyp.Literal(";").suppress()
877
    # this also converts the value to an int
878
    number = pyp.Word(pyp.nums).setParseAction(lambda s, l, t: int(t[0]))
879

    
880
    comment = pyp.Literal ("#") + pyp.Optional(pyp.restOfLine)
881
    defa = pyp.Literal("_is_default").suppress()
882
    dbl_quote = pyp.Literal('"').suppress()
883

    
884
    keyword = pyp.Word(pyp.alphanums + '-')
885

    
886
    # value types
887
    value = pyp.Word(pyp.alphanums + '_-/.:')
888
    quoted = dbl_quote + pyp.CharsNotIn('"') + dbl_quote
889
    addr_port = (pyp.Word(pyp.nums + '.') + pyp.Literal(':').suppress() +
890
                 number)
891
    # meta device, extended syntax
892
    meta_value = ((value ^ quoted) + pyp.Literal('[').suppress() +
893
                  number + pyp.Word(']').suppress())
894

    
895
    # a statement
896
    stmt = (~rbrace + keyword + ~lbrace +
897
            pyp.Optional(addr_port ^ value ^ quoted ^ meta_value) +
898
            pyp.Optional(defa) + semi +
899
            pyp.Optional(pyp.restOfLine).suppress())
900

    
901
    # an entire section
902
    section_name = pyp.Word(pyp.alphas + '_')
903
    section = section_name + lbrace + pyp.ZeroOrMore(pyp.Group(stmt)) + rbrace
904

    
905
    bnf = pyp.ZeroOrMore(pyp.Group(section ^ stmt))
906
    bnf.ignore(comment)
907

    
908
    cls._PARSE_SHOW = bnf
909

    
910
    return bnf
911

    
912
  @classmethod
913
  def _GetShowData(cls, minor):
914
    """Return the `drbdsetup show` data for a minor.
915

916
    """
917
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "show"])
918
    if result.failed:
919
      logger.Error("Can't display the drbd config: %s - %s" %
920
                   (result.fail_reason, result.output))
921
      return None
922
    return result.stdout
923

    
924
  @classmethod
925
  def _GetDevInfo(cls, out):
926
    """Parse details about a given DRBD minor.
927

928
    This return, if available, the local backing device (as a path)
929
    and the local and remote (ip, port) information from a string
930
    containing the output of the `drbdsetup show` command as returned
931
    by _GetShowData.
932

933
    """
934
    data = {}
935
    if not out:
936
      return data
937

    
938
    bnf = cls._GetShowParser()
939
    # run pyparse
940

    
941
    try:
942
      results = bnf.parseString(out)
943
    except pyp.ParseException, err:
944
      raise errors.BlockDeviceError("Can't parse drbdsetup show output: %s" %
945
                                    str(err))
946

    
947
    # and massage the results into our desired format
948
    for section in results:
949
      sname = section[0]
950
      if sname == "_this_host":
951
        for lst in section[1:]:
952
          if lst[0] == "disk":
953
            data["local_dev"] = lst[1]
954
          elif lst[0] == "meta-disk":
955
            data["meta_dev"] = lst[1]
956
            data["meta_index"] = lst[2]
957
          elif lst[0] == "address":
958
            data["local_addr"] = tuple(lst[1:])
959
      elif sname == "_remote_host":
960
        for lst in section[1:]:
961
          if lst[0] == "address":
962
            data["remote_addr"] = tuple(lst[1:])
963
    return data
964

    
965
  def _MatchesLocal(self, info):
966
    """Test if our local config matches with an existing device.
967

968
    The parameter should be as returned from `_GetDevInfo()`. This
969
    method tests if our local backing device is the same as the one in
970
    the info parameter, in effect testing if we look like the given
971
    device.
972

973
    """
974
    if self._children:
975
      backend, meta = self._children
976
    else:
977
      backend = meta = None
978

    
979
    if backend is not None:
980
      retval = ("local_dev" in info and info["local_dev"] == backend.dev_path)
981
    else:
982
      retval = ("local_dev" not in info)
983

    
984
    if meta is not None:
985
      retval = retval and ("meta_dev" in info and
986
                           info["meta_dev"] == meta.dev_path)
987
      retval = retval and ("meta_index" in info and
988
                           info["meta_index"] == 0)
989
    else:
990
      retval = retval and ("meta_dev" not in info and
991
                           "meta_index" not in info)
992
    return retval
993

    
994
  def _MatchesNet(self, info):
995
    """Test if our network config matches with an existing device.
996

997
    The parameter should be as returned from `_GetDevInfo()`. This
998
    method tests if our network configuration is the same as the one
999
    in the info parameter, in effect testing if we look like the given
1000
    device.
1001

1002
    """
1003
    if (((self._lhost is None and not ("local_addr" in info)) and
1004
         (self._rhost is None and not ("remote_addr" in info)))):
1005
      return True
1006

    
1007
    if self._lhost is None:
1008
      return False
1009

    
1010
    if not ("local_addr" in info and
1011
            "remote_addr" in info):
1012
      return False
1013

    
1014
    retval = (info["local_addr"] == (self._lhost, self._lport))
1015
    retval = (retval and
1016
              info["remote_addr"] == (self._rhost, self._rport))
1017
    return retval
1018

    
1019
  @classmethod
1020
  def _AssembleLocal(cls, minor, backend, meta):
1021
    """Configure the local part of a DRBD device.
1022

1023
    This is the first thing that must be done on an unconfigured DRBD
1024
    device. And it must be done only once.
1025

1026
    """
1027
    if not cls._IsValidMeta(meta):
1028
      return False
1029
    args = ["drbdsetup", cls._DevPath(minor), "disk",
1030
            backend, meta, "0", "-e", "detach", "--create-device"]
1031
    result = utils.RunCmd(args)
1032
    if result.failed:
1033
      logger.Error("Can't attach local disk: %s" % result.output)
1034
    return not result.failed
1035

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

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

    
1048
    args = ["drbdsetup", cls._DevPath(minor), "net",
1049
            "%s:%s" % (lhost, lport), "%s:%s" % (rhost, rport), protocol,
1050
            "-A", "discard-zero-changes",
1051
            "-B", "consensus",
1052
            "--create-device",
1053
            ]
1054
    if dual_pri:
1055
      args.append("-m")
1056
    if hmac and secret:
1057
      args.extend(["-a", hmac, "-x", secret])
1058
    result = utils.RunCmd(args)
1059
    if result.failed:
1060
      logger.Error("Can't setup network for dbrd device: %s - %s" %
1061
                   (result.fail_reason, result.output))
1062
      return False
1063

    
1064
    timeout = time.time() + 10
1065
    ok = False
1066
    while time.time() < timeout:
1067
      info = cls._GetDevInfo(cls._GetShowData(minor))
1068
      if not "local_addr" in info or not "remote_addr" in info:
1069
        time.sleep(1)
1070
        continue
1071
      if (info["local_addr"] != (lhost, lport) or
1072
          info["remote_addr"] != (rhost, rport)):
1073
        time.sleep(1)
1074
        continue
1075
      ok = True
1076
      break
1077
    if not ok:
1078
      logger.Error("Timeout while configuring network")
1079
      return False
1080
    return True
1081

    
1082
  def AddChildren(self, devices):
1083
    """Add a disk to the DRBD device.
1084

1085
    """
1086
    if self.minor is None:
1087
      raise errors.BlockDeviceError("Can't attach to dbrd8 during AddChildren")
1088
    if len(devices) != 2:
1089
      raise errors.BlockDeviceError("Need two devices for AddChildren")
1090
    info = self._GetDevInfo(self._GetShowData(self.minor))
1091
    if "local_dev" in info:
1092
      raise errors.BlockDeviceError("DRBD8 already attached to a local disk")
1093
    backend, meta = devices
1094
    if backend.dev_path is None or meta.dev_path is None:
1095
      raise errors.BlockDeviceError("Children not ready during AddChildren")
1096
    backend.Open()
1097
    meta.Open()
1098
    if not self._CheckMetaSize(meta.dev_path):
1099
      raise errors.BlockDeviceError("Invalid meta device size")
1100
    self._InitMeta(self._FindUnusedMinor(), meta.dev_path)
1101
    if not self._IsValidMeta(meta.dev_path):
1102
      raise errors.BlockDeviceError("Cannot initalize meta device")
1103

    
1104
    if not self._AssembleLocal(self.minor, backend.dev_path, meta.dev_path):
1105
      raise errors.BlockDeviceError("Can't attach to local storage")
1106
    self._children = devices
1107

    
1108
  def RemoveChildren(self, devices):
1109
    """Detach the drbd device from local storage.
1110

1111
    """
1112
    if self.minor is None:
1113
      raise errors.BlockDeviceError("Can't attach to drbd8 during"
1114
                                    " RemoveChildren")
1115
    # early return if we don't actually have backing storage
1116
    info = self._GetDevInfo(self._GetShowData(self.minor))
1117
    if "local_dev" not in info:
1118
      return
1119
    if len(self._children) != 2:
1120
      raise errors.BlockDeviceError("We don't have two children: %s" %
1121
                                    self._children)
1122
    if self._children.count(None) == 2: # we don't actually have children :)
1123
      logger.Error("Requested detach while detached")
1124
      return
1125
    if len(devices) != 2:
1126
      raise errors.BlockDeviceError("We need two children in RemoveChildren")
1127
    for child, dev in zip(self._children, devices):
1128
      if dev != child.dev_path:
1129
        raise errors.BlockDeviceError("Mismatch in local storage"
1130
                                      " (%s != %s) in RemoveChildren" %
1131
                                      (dev, child.dev_path))
1132

    
1133
    if not self._ShutdownLocal(self.minor):
1134
      raise errors.BlockDeviceError("Can't detach from local storage")
1135
    self._children = []
1136

    
1137
  def SetSyncSpeed(self, kbytes):
1138
    """Set the speed of the DRBD syncer.
1139

1140
    """
1141
    children_result = super(DRBD8, self).SetSyncSpeed(kbytes)
1142
    if self.minor is None:
1143
      logger.Info("Instance not attached to a device")
1144
      return False
1145
    result = utils.RunCmd(["drbdsetup", self.dev_path, "syncer", "-r", "%d" %
1146
                           kbytes])
1147
    if result.failed:
1148
      logger.Error("Can't change syncer rate: %s - %s" %
1149
                   (result.fail_reason, result.output))
1150
    return not result.failed and children_result
1151

    
1152
  def GetProcStatus(self):
1153
    """Return device data from /proc.
1154

1155
    """
1156
    if self.minor is None:
1157
      raise errors.BlockDeviceError("GetStats() called while not attached")
1158
    proc_info = self._MassageProcData(self._GetProcData())
1159
    if self.minor not in proc_info:
1160
      raise errors.BlockDeviceError("Can't find myself in /proc (minor %d)" %
1161
                                    self.minor)
1162
    return DRBD8Status(proc_info[self.minor])
1163

    
1164
  def GetSyncStatus(self):
1165
    """Returns the sync status of the device.
1166

1167
    Returns:
1168
     (sync_percent, estimated_time, is_degraded)
1169

1170
    If sync_percent is None, it means all is ok
1171
    If estimated_time is None, it means we can't esimate
1172
    the time needed, otherwise it's the time left in seconds.
1173

1174

1175
    We set the is_degraded parameter to True on two conditions:
1176
    network not connected or local disk missing.
1177

1178
    We compute the ldisk parameter based on wheter we have a local
1179
    disk or not.
1180

1181
    """
1182
    if self.minor is None and not self.Attach():
1183
      raise errors.BlockDeviceError("Can't attach to device in GetSyncStatus")
1184
    stats = self.GetProcStatus()
1185
    ldisk = not stats.is_disk_uptodate
1186
    is_degraded = not stats.is_connected
1187
    return stats.sync_percent, stats.est_time, is_degraded or ldisk, ldisk
1188

    
1189
  def Open(self, force=False):
1190
    """Make the local state primary.
1191

1192
    If the 'force' parameter is given, the '-o' option is passed to
1193
    drbdsetup. Since this is a potentially dangerous operation, the
1194
    force flag should be only given after creation, when it actually
1195
    is mandatory.
1196

1197
    """
1198
    if self.minor is None and not self.Attach():
1199
      logger.Error("DRBD cannot attach to a device during open")
1200
      return False
1201
    cmd = ["drbdsetup", self.dev_path, "primary"]
1202
    if force:
1203
      cmd.append("-o")
1204
    result = utils.RunCmd(cmd)
1205
    if result.failed:
1206
      msg = ("Can't make drbd device primary: %s" % result.output)
1207
      logger.Error(msg)
1208
      raise errors.BlockDeviceError(msg)
1209

    
1210
  def Close(self):
1211
    """Make the local state secondary.
1212

1213
    This will, of course, fail if the device is in use.
1214

1215
    """
1216
    if self.minor is None and not self.Attach():
1217
      logger.Info("Instance not attached to a device")
1218
      raise errors.BlockDeviceError("Can't find device")
1219
    result = utils.RunCmd(["drbdsetup", self.dev_path, "secondary"])
1220
    if result.failed:
1221
      msg = ("Can't switch drbd device to"
1222
             " secondary: %s" % result.output)
1223
      logger.Error(msg)
1224
      raise errors.BlockDeviceError(msg)
1225

    
1226
  def Attach(self):
1227
    """Find a DRBD device which matches our config and attach to it.
1228

1229
    In case of partially attached (local device matches but no network
1230
    setup), we perform the network attach. If successful, we re-test
1231
    the attach if can return success.
1232

1233
    """
1234
    for minor in self._GetUsedDevs():
1235
      info = self._GetDevInfo(self._GetShowData(minor))
1236
      match_l = self._MatchesLocal(info)
1237
      match_r = self._MatchesNet(info)
1238
      if match_l and match_r:
1239
        break
1240
      if match_l and not match_r and "local_addr" not in info:
1241
        res_r = self._AssembleNet(minor,
1242
                                  (self._lhost, self._lport,
1243
                                   self._rhost, self._rport),
1244
                                  "C")
1245
        if res_r:
1246
          if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1247
            break
1248
      # the weakest case: we find something that is only net attached
1249
      # even though we were passed some children at init time
1250
      if match_r and "local_dev" not in info:
1251
        break
1252

    
1253
      # this case must be considered only if we actually have local
1254
      # storage, i.e. not in diskless mode, because all diskless
1255
      # devices are equal from the point of view of local
1256
      # configuration
1257
      if (match_l and "local_dev" in info and
1258
          not match_r and "local_addr" in info):
1259
        # strange case - the device network part points to somewhere
1260
        # else, even though its local storage is ours; as we own the
1261
        # drbd space, we try to disconnect from the remote peer and
1262
        # reconnect to our correct one
1263
        if not self._ShutdownNet(minor):
1264
          raise errors.BlockDeviceError("Device has correct local storage,"
1265
                                        " wrong remote peer and is unable to"
1266
                                        " disconnect in order to attach to"
1267
                                        " the correct peer")
1268
        # note: _AssembleNet also handles the case when we don't want
1269
        # local storage (i.e. one or more of the _[lr](host|port) is
1270
        # None)
1271
        if (self._AssembleNet(minor, (self._lhost, self._lport,
1272
                                      self._rhost, self._rport), "C") and
1273
            self._MatchesNet(self._GetDevInfo(self._GetShowData(minor)))):
1274
          break
1275

    
1276
    else:
1277
      minor = None
1278

    
1279
    self._SetFromMinor(minor)
1280
    return minor is not None
1281

    
1282
  def Assemble(self):
1283
    """Assemble the drbd.
1284

1285
    Method:
1286
      - if we have a local backing device, we bind to it by:
1287
        - checking the list of used drbd devices
1288
        - check if the local minor use of any of them is our own device
1289
        - if yes, abort?
1290
        - if not, bind
1291
      - if we have a local/remote net info:
1292
        - redo the local backing device step for the remote device
1293
        - check if any drbd device is using the local port,
1294
          if yes abort
1295
        - check if any remote drbd device is using the remote
1296
          port, if yes abort (for now)
1297
        - bind our net port
1298
        - bind the remote net port
1299

1300
    """
1301
    self.Attach()
1302
    if self.minor is not None:
1303
      logger.Info("Already assembled")
1304
      return True
1305

    
1306
    result = super(DRBD8, self).Assemble()
1307
    if not result:
1308
      return result
1309

    
1310
    minor = self._FindUnusedMinor()
1311
    need_localdev_teardown = False
1312
    if self._children and self._children[0] and self._children[1]:
1313
      result = self._AssembleLocal(minor, self._children[0].dev_path,
1314
                                   self._children[1].dev_path)
1315
      if not result:
1316
        return False
1317
      need_localdev_teardown = True
1318
    if self._lhost and self._lport and self._rhost and self._rport:
1319
      result = self._AssembleNet(minor,
1320
                                 (self._lhost, self._lport,
1321
                                  self._rhost, self._rport),
1322
                                 "C")
1323
      if not result:
1324
        if need_localdev_teardown:
1325
          # we will ignore failures from this
1326
          logger.Error("net setup failed, tearing down local device")
1327
          self._ShutdownAll(minor)
1328
        return False
1329
    self._SetFromMinor(minor)
1330
    return True
1331

    
1332
  @classmethod
1333
  def _ShutdownLocal(cls, minor):
1334
    """Detach from the local device.
1335

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

1339
    """
1340
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "detach"])
1341
    if result.failed:
1342
      logger.Error("Can't detach local device: %s" % result.output)
1343
    return not result.failed
1344

    
1345
  @classmethod
1346
  def _ShutdownNet(cls, minor):
1347
    """Disconnect from the remote peer.
1348

1349
    This fails if we don't have a local device.
1350

1351
    """
1352
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disconnect"])
1353
    if result.failed:
1354
      logger.Error("Can't shutdown network: %s" % result.output)
1355
    return not result.failed
1356

    
1357
  @classmethod
1358
  def _ShutdownAll(cls, minor):
1359
    """Deactivate the device.
1360

1361
    This will, of course, fail if the device is in use.
1362

1363
    """
1364
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "down"])
1365
    if result.failed:
1366
      logger.Error("Can't shutdown drbd device: %s" % result.output)
1367
    return not result.failed
1368

    
1369
  def Shutdown(self):
1370
    """Shutdown the DRBD device.
1371

1372
    """
1373
    if self.minor is None and not self.Attach():
1374
      logger.Info("DRBD device not attached to a device during Shutdown")
1375
      return True
1376
    if not self._ShutdownAll(self.minor):
1377
      return False
1378
    self.minor = None
1379
    self.dev_path = None
1380
    return True
1381

    
1382
  def Remove(self):
1383
    """Stub remove for DRBD devices.
1384

1385
    """
1386
    return self.Shutdown()
1387

    
1388
  @classmethod
1389
  def Create(cls, unique_id, children, size):
1390
    """Create a new DRBD8 device.
1391

1392
    Since DRBD devices are not created per se, just assembled, this
1393
    function only initializes the metadata.
1394

1395
    """
1396
    if len(children) != 2:
1397
      raise errors.ProgrammerError("Invalid setup for the drbd device")
1398
    meta = children[1]
1399
    meta.Assemble()
1400
    if not meta.Attach():
1401
      raise errors.BlockDeviceError("Can't attach to meta device")
1402
    if not cls._CheckMetaSize(meta.dev_path):
1403
      raise errors.BlockDeviceError("Invalid meta device size")
1404
    cls._InitMeta(cls._FindUnusedMinor(), meta.dev_path)
1405
    if not cls._IsValidMeta(meta.dev_path):
1406
      raise errors.BlockDeviceError("Cannot initalize meta device")
1407
    return cls(unique_id, children)
1408

    
1409
  def Grow(self, amount):
1410
    """Resize the DRBD device and its backing storage.
1411

1412
    """
1413
    if self.minor is None:
1414
      raise errors.ProgrammerError("drbd8: Grow called while not attached")
1415
    if len(self._children) != 2 or None in self._children:
1416
      raise errors.BlockDeviceError("Cannot grow diskless DRBD8 device")
1417
    self._children[0].Grow(amount)
1418
    result = utils.RunCmd(["drbdsetup", self.dev_path, "resize"])
1419
    if result.failed:
1420
      raise errors.BlockDeviceError("resize failed for %s: %s" %
1421
                                    (self.dev_path, result.output))
1422
    return
1423

    
1424

    
1425
class FileStorage(BlockDev):
1426
  """File device.
1427

1428
  This class represents the a file storage backend device.
1429

1430
  The unique_id for the file device is a (file_driver, file_path) tuple.
1431

1432
  """
1433
  def __init__(self, unique_id, children):
1434
    """Initalizes a file device backend.
1435

1436
    """
1437
    if children:
1438
      raise errors.BlockDeviceError("Invalid setup for file device")
1439
    super(FileStorage, self).__init__(unique_id, children)
1440
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1441
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1442
    self.driver = unique_id[0]
1443
    self.dev_path = unique_id[1]
1444

    
1445
  def Assemble(self):
1446
    """Assemble the device.
1447

1448
    Checks whether the file device exists, raises BlockDeviceError otherwise.
1449

1450
    """
1451
    if not os.path.exists(self.dev_path):
1452
      raise errors.BlockDeviceError("File device '%s' does not exist." %
1453
                                    self.dev_path)
1454
    return True
1455

    
1456
  def Shutdown(self):
1457
    """Shutdown the device.
1458

1459
    This is a no-op for the file type, as we don't deacivate
1460
    the file on shutdown.
1461

1462
    """
1463
    return True
1464

    
1465
  def Open(self, force=False):
1466
    """Make the device ready for I/O.
1467

1468
    This is a no-op for the file type.
1469

1470
    """
1471
    pass
1472

    
1473
  def Close(self):
1474
    """Notifies that the device will no longer be used for I/O.
1475

1476
    This is a no-op for the file type.
1477

1478
    """
1479
    pass
1480

    
1481
  def Remove(self):
1482
    """Remove the file backing the block device.
1483

1484
    Returns:
1485
      boolean indicating wheter removal of file was successful or not.
1486

1487
    """
1488
    if not os.path.exists(self.dev_path):
1489
      return True
1490
    try:
1491
      os.remove(self.dev_path)
1492
      return True
1493
    except OSError, err:
1494
      logger.Error("Can't remove file '%s': %s"
1495
                   % (self.dev_path, err))
1496
      return False
1497

    
1498
  def Attach(self):
1499
    """Attach to an existing file.
1500

1501
    Check if this file already exists.
1502

1503
    Returns:
1504
      boolean indicating if file exists or not.
1505

1506
    """
1507
    if os.path.exists(self.dev_path):
1508
      return True
1509
    return False
1510

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

1515
    Args:
1516
      children:
1517
      size: integer size of file in MiB
1518

1519
    Returns:
1520
      A ganeti.bdev.FileStorage object.
1521

1522
    """
1523
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1524
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1525
    dev_path = unique_id[1]
1526
    try:
1527
      f = open(dev_path, 'w')
1528
    except IOError, err:
1529
      raise errors.BlockDeviceError("Could not create '%'" % err)
1530
    else:
1531
      f.truncate(size * 1024 * 1024)
1532
      f.close()
1533

    
1534
    return FileStorage(unique_id, children)
1535

    
1536

    
1537
DEV_MAP = {
1538
  constants.LD_LV: LogicalVolume,
1539
  constants.LD_DRBD8: DRBD8,
1540
  constants.LD_FILE: FileStorage,
1541
  }
1542

    
1543

    
1544
def FindDevice(dev_type, unique_id, children):
1545
  """Search for an existing, assembled device.
1546

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

1550
  """
1551
  if dev_type not in DEV_MAP:
1552
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1553
  device = DEV_MAP[dev_type](unique_id, children)
1554
  if not device.Attach():
1555
    return None
1556
  return  device
1557

    
1558

    
1559
def AttachOrAssemble(dev_type, unique_id, children):
1560
  """Try to attach or assemble an existing device.
1561

1562
  This will attach to an existing assembled device or will assemble
1563
  the device, as needed, to bring it fully up.
1564

1565
  """
1566
  if dev_type not in DEV_MAP:
1567
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1568
  device = DEV_MAP[dev_type](unique_id, children)
1569
  if not device.Attach():
1570
    device.Assemble()
1571
    if not device.Attach():
1572
      raise errors.BlockDeviceError("Can't find a valid block device for"
1573
                                    " %s/%s/%s" %
1574
                                    (dev_type, unique_id, children))
1575
  return device
1576

    
1577

    
1578
def Create(dev_type, unique_id, children, size):
1579
  """Create a device.
1580

1581
  """
1582
  if dev_type not in DEV_MAP:
1583
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1584
  device = DEV_MAP[dev_type].Create(unique_id, children, size)
1585
  return device