Statistics
| Branch: | Tag: | Revision:

root / lib / bdev.py @ 4300c4b6

History | View | Annotate | Download (49.4 kB)

1
#
2
#
3

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

    
21

    
22
"""Block device abstraction"""
23

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

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

    
35

    
36
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
    self.attached = False
86

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

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

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

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

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

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

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

124
    """
125
    raise NotImplementedError
126

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

130
    """
131
    raise NotImplementedError
132

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

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

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

144
    """
145
    raise NotImplementedError
146

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

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

154
    """
155
    raise NotImplementedError
156

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

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

162
    """
163
    raise NotImplementedError
164

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

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

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

174
    """
175
    raise NotImplementedError
176

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

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

184
    """
185
    raise NotImplementedError
186

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

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

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

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

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

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

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

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

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

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

221
    """
222
    return None, None, False, False
223

    
224

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

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

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

    
249

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

253
    Only supported for some device types.
254

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

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

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

265
    Returns: None
266

267
    """
268
    raise NotImplementedError
269

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

    
275

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

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

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

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

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

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

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

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

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

329
    Args:
330
      vg_name: the volume group name
331

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

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

    
355
    return data
356

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

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

    
370
    return not result.failed
371

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

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

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

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

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

    
412
    status, major, minor = out[:3]
413
    if len(status) != 6:
414
      logging.error("lvs lv_attr is not 6 characters (%s)", status)
415
      return False
416

    
417
    try:
418
      major = int(major)
419
      minor = int(minor)
420
    except ValueError, err:
421
      logging.error("lvs major/minor cannot be parsed: %s", str(err))
422

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

    
430
  def Assemble(self):
431
    """Assemble the device.
432

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

437
    """
438
    result = utils.RunCmd(["lvchange", "-ay", self.dev_path])
439
    if result.failed:
440
      logging.error("Can't activate lv %s: %s", self.dev_path, result.output)
441
      return False
442
    return self.Attach()
443

    
444
  def Shutdown(self):
445
    """Shutdown the device.
446

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

450
    """
451
    return True
452

    
453
  def GetSyncStatus(self):
454
    """Returns the sync status of the device.
455

456
    If this device is a mirroring device, this function returns the
457
    status of the mirror.
458

459
    Returns:
460
     (sync_percent, estimated_time, is_degraded, ldisk)
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
    """
476
    return None, None, self._degraded, self._degraded
477

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

481
    This is a no-op for the LV device type.
482

483
    """
484
    pass
485

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

489
    This is a no-op for the LV device type.
490

491
    """
492
    pass
493

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

497
    """
498
    snap_name = self._lv_name + ".snap"
499

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

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

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

    
522
    return snap_name
523

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

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

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

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

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

546
    """
547
    # we try multiple algorithms since the 'best' ones might not have
548
    # space available in the right place, but later ones might (since
549
    # they have less constraints); also note that only recent LVM
550
    # supports 'cling'
551
    for alloc_policy in "contiguous", "cling", "normal":
552
      result = utils.RunCmd(["lvextend", "--alloc", alloc_policy,
553
                             "-L", "+%dm" % amount, self.dev_path])
554
      if not result.failed:
555
        return
556
    raise errors.BlockDeviceError("Can't grow LV %s: %s" %
557
                                  (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
  LINE_RE = re.compile(r"\s*[0-9]+:\s*cs:(\S+)\s+st:([^/]+)/(\S+)"
567
                       "\s+ds:([^/]+)/(\S+)\s+.*$")
568
  SYNC_RE = re.compile(r"^.*\ssync'ed:\s*([0-9.]+)%.*"
569
                       "\sfinish: ([0-9]+):([0-9]+):([0-9]+)\s.*$")
570

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

    
581
    self.is_standalone = self.cstatus == "StandAlone"
582
    self.is_wfconn = self.cstatus == "WFConnection"
583
    self.is_connected = self.cstatus == "Connected"
584
    self.is_primary = self.lrole == "Primary"
585
    self.is_secondary = self.lrole == "Secondary"
586
    self.peer_primary = self.rrole == "Primary"
587
    self.peer_secondary = self.rrole == "Secondary"
588
    self.both_primary = self.is_primary and self.peer_primary
589
    self.both_secondary = self.is_secondary and self.peer_secondary
590

    
591
    self.is_diskless = self.ldisk == "Diskless"
592
    self.is_disk_uptodate = self.ldisk == "UpToDate"
593

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

    
605
    self.is_sync_target = self.peer_sync_source = self.cstatus == "SyncTarget"
606
    self.peer_sync_target = self.is_sync_source = self.cstatus == "SyncSource"
607
    self.is_resync = self.is_sync_target or self.is_sync_source
608

    
609

    
610
class BaseDRBD(BlockDev):
611
  """Base DRBD class.
612

613
  This class contains a few bits of common functionality between the
614
  0.7 and 8.x versions of DRBD.
615

616
  """
617
  _VERSION_RE = re.compile(r"^version: (\d+)\.(\d+)\.(\d+)"
618
                           r" \(api:(\d+)/proto:(\d+)(?:-(\d+))?\)")
619

    
620
  _DRBD_MAJOR = 147
621
  _ST_UNCONFIGURED = "Unconfigured"
622
  _ST_WFCONNECTION = "WFConnection"
623
  _ST_CONNECTED = "Connected"
624

    
625
  _STATUS_FILE = "/proc/drbd"
626

    
627
  @staticmethod
628
  def _GetProcData(filename=_STATUS_FILE):
629
    """Return data from /proc/drbd.
630

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

    
641
  @staticmethod
642
  def _MassageProcData(data):
643
    """Transform the output of _GetProdData into a nicer form.
644

645
    Returns:
646
      a dictionary of minor: joined lines from /proc/drbd for that minor
647

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

    
667
  @classmethod
668
  def _GetVersion(cls):
669
    """Return the DRBD version.
670

671
    This will return a dict with keys:
672
      k_major,
673
      k_minor,
674
      k_point,
675
      api,
676
      proto,
677
      proto2 (only on drbd > 8.2.X)
678

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

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

    
697
    return retval
698

    
699
  @staticmethod
700
  def _DevPath(minor):
701
    """Return the path to a drbd device for a given minor.
702

703
    """
704
    return "/dev/drbd%d" % minor
705

    
706
  @classmethod
707
  def _GetUsedDevs(cls):
708
    """Compute the list of used DRBD devices.
709

710
    """
711
    data = cls._GetProcData()
712

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

    
725
    return used_devs
726

    
727
  def _SetFromMinor(self, minor):
728
    """Set our parameters based on the given minor.
729

730
    This sets our minor variable and our dev_path.
731

732
    """
733
    if minor is None:
734
      self.minor = self.dev_path = None
735
      self.attached = False
736
    else:
737
      self.minor = minor
738
      self.dev_path = self._DevPath(minor)
739
      self.attached = True
740

    
741
  @staticmethod
742
  def _CheckMetaSize(meta_device):
743
    """Check if the given meta device looks like a valid one.
744

745
    This currently only check the size, which must be around
746
    128MiB.
747

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

    
768
  def Rename(self, new_id):
769
    """Rename a device.
770

771
    This is not supported for drbd devices.
772

773
    """
774
    raise errors.ProgrammerError("Can't rename a drbd device")
775

    
776

    
777
class DRBD8(BaseDRBD):
778
  """DRBD v8.x block device.
779

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

784
  The unique_id for the drbd device is the (local_ip, local_port,
785
  remote_ip, remote_port) tuple, and it must have two children: the
786
  data device and the meta_device. The meta device is checked for
787
  valid size and is zeroed on create.
788

789
  """
790
  _MAX_MINORS = 255
791
  _PARSE_SHOW = None
792

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

    
805
    if len(children) not in (0, 2):
806
      raise ValueError("Invalid configuration data %s" % str(children))
807
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 4:
808
      raise ValueError("Invalid configuration data %s" % str(unique_id))
809
    self._lhost, self._lport, self._rhost, self._rport = unique_id
810
    self.Attach()
811

    
812
  @classmethod
813
  def _InitMeta(cls, minor, dev_path):
814
    """Initialize a meta device.
815

816
    This will not work if the given minor is in use.
817

818
    """
819
    result = utils.RunCmd(["drbdmeta", "--force", cls._DevPath(minor),
820
                           "v08", dev_path, "0", "create-md"])
821
    if result.failed:
822
      raise errors.BlockDeviceError("Can't initialize meta device: %s" %
823
                                    result.output)
824

    
825
  @classmethod
826
  def _FindUnusedMinor(cls):
827
    """Find an unused DRBD device.
828

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

832
    """
833
    data = cls._GetProcData()
834

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

    
853
  @classmethod
854
  def _IsValidMeta(cls, meta_device):
855
    """Check if the given meta device looks like a valid one.
856

857
    """
858
    minor = cls._FindUnusedMinor()
859
    minor_path = cls._DevPath(minor)
860
    result = utils.RunCmd(["drbdmeta", minor_path,
861
                           "v08", meta_device, "0",
862
                           "dstate"])
863
    if result.failed:
864
      logging.error("Invalid meta device %s: %s", meta_device, result.output)
865
      return False
866
    return True
867

    
868
  @classmethod
869
  def _GetShowParser(cls):
870
    """Return a parser for `drbd show` output.
871

872
    This will either create or return an already-create parser for the
873
    output of the command `drbd show`.
874

875
    """
876
    if cls._PARSE_SHOW is not None:
877
      return cls._PARSE_SHOW
878

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

    
886
    comment = pyp.Literal ("#") + pyp.Optional(pyp.restOfLine)
887
    defa = pyp.Literal("_is_default").suppress()
888
    dbl_quote = pyp.Literal('"').suppress()
889

    
890
    keyword = pyp.Word(pyp.alphanums + '-')
891

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

    
901
    # a statement
902
    stmt = (~rbrace + keyword + ~lbrace +
903
            pyp.Optional(addr_port ^ value ^ quoted ^ meta_value) +
904
            pyp.Optional(defa) + semi +
905
            pyp.Optional(pyp.restOfLine).suppress())
906

    
907
    # an entire section
908
    section_name = pyp.Word(pyp.alphas + '_')
909
    section = section_name + lbrace + pyp.ZeroOrMore(pyp.Group(stmt)) + rbrace
910

    
911
    bnf = pyp.ZeroOrMore(pyp.Group(section ^ stmt))
912
    bnf.ignore(comment)
913

    
914
    cls._PARSE_SHOW = bnf
915

    
916
    return bnf
917

    
918
  @classmethod
919
  def _GetShowData(cls, minor):
920
    """Return the `drbdsetup show` data for a minor.
921

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

    
930
  @classmethod
931
  def _GetDevInfo(cls, out):
932
    """Parse details about a given DRBD minor.
933

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

939
    """
940
    data = {}
941
    if not out:
942
      return data
943

    
944
    bnf = cls._GetShowParser()
945
    # run pyparse
946

    
947
    try:
948
      results = bnf.parseString(out)
949
    except pyp.ParseException, err:
950
      raise errors.BlockDeviceError("Can't parse drbdsetup show output: %s" %
951
                                    str(err))
952

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

    
971
  def _MatchesLocal(self, info):
972
    """Test if our local config matches with an existing device.
973

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

979
    """
980
    if self._children:
981
      backend, meta = self._children
982
    else:
983
      backend = meta = None
984

    
985
    if backend is not None:
986
      retval = ("local_dev" in info and info["local_dev"] == backend.dev_path)
987
    else:
988
      retval = ("local_dev" not in info)
989

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

    
1000
  def _MatchesNet(self, info):
1001
    """Test if our network config matches with an existing device.
1002

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

1008
    """
1009
    if (((self._lhost is None and not ("local_addr" in info)) and
1010
         (self._rhost is None and not ("remote_addr" in info)))):
1011
      return True
1012

    
1013
    if self._lhost is None:
1014
      return False
1015

    
1016
    if not ("local_addr" in info and
1017
            "remote_addr" in info):
1018
      return False
1019

    
1020
    retval = (info["local_addr"] == (self._lhost, self._lport))
1021
    retval = (retval and
1022
              info["remote_addr"] == (self._rhost, self._rport))
1023
    return retval
1024

    
1025
  @classmethod
1026
  def _AssembleLocal(cls, minor, backend, meta):
1027
    """Configure the local part of a DRBD device.
1028

1029
    This is the first thing that must be done on an unconfigured DRBD
1030
    device. And it must be done only once.
1031

1032
    """
1033
    if not cls._IsValidMeta(meta):
1034
      return False
1035
    args = ["drbdsetup", cls._DevPath(minor), "disk",
1036
            backend, meta, "0", "-e", "detach", "--create-device"]
1037
    result = utils.RunCmd(args)
1038
    if result.failed:
1039
      logging.error("Can't attach local disk: %s", result.output)
1040
    return not result.failed
1041

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

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

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

    
1070
    timeout = time.time() + 10
1071
    ok = False
1072
    while time.time() < timeout:
1073
      info = cls._GetDevInfo(cls._GetShowData(minor))
1074
      if not "local_addr" in info or not "remote_addr" in info:
1075
        time.sleep(1)
1076
        continue
1077
      if (info["local_addr"] != (lhost, lport) or
1078
          info["remote_addr"] != (rhost, rport)):
1079
        time.sleep(1)
1080
        continue
1081
      ok = True
1082
      break
1083
    if not ok:
1084
      logging.error("Timeout while configuring network")
1085
      return False
1086
    return True
1087

    
1088
  def AddChildren(self, devices):
1089
    """Add a disk to the DRBD device.
1090

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

    
1110
    if not self._AssembleLocal(self.minor, backend.dev_path, meta.dev_path):
1111
      raise errors.BlockDeviceError("Can't attach to local storage")
1112
    self._children = devices
1113

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

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

    
1139
    if not self._ShutdownLocal(self.minor):
1140
      raise errors.BlockDeviceError("Can't detach from local storage")
1141
    self._children = []
1142

    
1143
  def SetSyncSpeed(self, kbytes):
1144
    """Set the speed of the DRBD syncer.
1145

1146
    """
1147
    children_result = super(DRBD8, self).SetSyncSpeed(kbytes)
1148
    if self.minor is None:
1149
      logging.info("Instance not attached to a device")
1150
      return False
1151
    result = utils.RunCmd(["drbdsetup", self.dev_path, "syncer", "-r", "%d" %
1152
                           kbytes])
1153
    if result.failed:
1154
      logging.error("Can't change syncer rate: %s - %s",
1155
                    result.fail_reason, result.output)
1156
    return not result.failed and children_result
1157

    
1158
  def GetProcStatus(self):
1159
    """Return device data from /proc.
1160

1161
    """
1162
    if self.minor is None:
1163
      raise errors.BlockDeviceError("GetStats() called while not attached")
1164
    proc_info = self._MassageProcData(self._GetProcData())
1165
    if self.minor not in proc_info:
1166
      raise errors.BlockDeviceError("Can't find myself in /proc (minor %d)" %
1167
                                    self.minor)
1168
    return DRBD8Status(proc_info[self.minor])
1169

    
1170
  def GetSyncStatus(self):
1171
    """Returns the sync status of the device.
1172

1173
    Returns:
1174
     (sync_percent, estimated_time, is_degraded)
1175

1176
    If sync_percent is None, it means all is ok
1177
    If estimated_time is None, it means we can't esimate
1178
    the time needed, otherwise it's the time left in seconds.
1179

1180

1181
    We set the is_degraded parameter to True on two conditions:
1182
    network not connected or local disk missing.
1183

1184
    We compute the ldisk parameter based on wheter we have a local
1185
    disk or not.
1186

1187
    """
1188
    if self.minor is None and not self.Attach():
1189
      raise errors.BlockDeviceError("Can't attach to device in GetSyncStatus")
1190
    stats = self.GetProcStatus()
1191
    ldisk = not stats.is_disk_uptodate
1192
    is_degraded = not stats.is_connected
1193
    return stats.sync_percent, stats.est_time, is_degraded or ldisk, ldisk
1194

    
1195
  def Open(self, force=False):
1196
    """Make the local state primary.
1197

1198
    If the 'force' parameter is given, the '-o' option is passed to
1199
    drbdsetup. Since this is a potentially dangerous operation, the
1200
    force flag should be only given after creation, when it actually
1201
    is mandatory.
1202

1203
    """
1204
    if self.minor is None and not self.Attach():
1205
      logging.error("DRBD cannot attach to a device during open")
1206
      return False
1207
    cmd = ["drbdsetup", self.dev_path, "primary"]
1208
    if force:
1209
      cmd.append("-o")
1210
    result = utils.RunCmd(cmd)
1211
    if result.failed:
1212
      msg = ("Can't make drbd device primary: %s" % result.output)
1213
      logging.error(msg)
1214
      raise errors.BlockDeviceError(msg)
1215

    
1216
  def Close(self):
1217
    """Make the local state secondary.
1218

1219
    This will, of course, fail if the device is in use.
1220

1221
    """
1222
    if self.minor is None and not self.Attach():
1223
      logging.info("Instance not attached to a device")
1224
      raise errors.BlockDeviceError("Can't find device")
1225
    result = utils.RunCmd(["drbdsetup", self.dev_path, "secondary"])
1226
    if result.failed:
1227
      msg = ("Can't switch drbd device to"
1228
             " secondary: %s" % result.output)
1229
      logging.error(msg)
1230
      raise errors.BlockDeviceError(msg)
1231

    
1232
  def Attach(self):
1233
    """Find a DRBD device which matches our config and attach to it.
1234

1235
    In case of partially attached (local device matches but no network
1236
    setup), we perform the network attach. If successful, we re-test
1237
    the attach if can return success.
1238

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

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

    
1282
    else:
1283
      minor = None
1284

    
1285
    self._SetFromMinor(minor)
1286
    return minor is not None
1287

    
1288
  def Assemble(self):
1289
    """Assemble the drbd.
1290

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

1306
    """
1307
    self.Attach()
1308
    if self.minor is not None:
1309
      logging.info("Already assembled")
1310
      return True
1311

    
1312
    result = super(DRBD8, self).Assemble()
1313
    if not result:
1314
      return result
1315

    
1316
    minor = self._FindUnusedMinor()
1317
    need_localdev_teardown = False
1318
    if self._children and self._children[0] and self._children[1]:
1319
      result = self._AssembleLocal(minor, self._children[0].dev_path,
1320
                                   self._children[1].dev_path)
1321
      if not result:
1322
        return False
1323
      need_localdev_teardown = True
1324
    if self._lhost and self._lport and self._rhost and self._rport:
1325
      result = self._AssembleNet(minor,
1326
                                 (self._lhost, self._lport,
1327
                                  self._rhost, self._rport),
1328
                                 "C")
1329
      if not result:
1330
        if need_localdev_teardown:
1331
          # we will ignore failures from this
1332
          logging.error("net setup failed, tearing down local device")
1333
          self._ShutdownAll(minor)
1334
        return False
1335
    self._SetFromMinor(minor)
1336
    return True
1337

    
1338
  @classmethod
1339
  def _ShutdownLocal(cls, minor):
1340
    """Detach from the local device.
1341

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

1345
    """
1346
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "detach"])
1347
    if result.failed:
1348
      logging.error("Can't detach local device: %s", result.output)
1349
    return not result.failed
1350

    
1351
  @classmethod
1352
  def _ShutdownNet(cls, minor):
1353
    """Disconnect from the remote peer.
1354

1355
    This fails if we don't have a local device.
1356

1357
    """
1358
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disconnect"])
1359
    if result.failed:
1360
      logging.error("Can't shutdown network: %s", result.output)
1361
    return not result.failed
1362

    
1363
  @classmethod
1364
  def _ShutdownAll(cls, minor):
1365
    """Deactivate the device.
1366

1367
    This will, of course, fail if the device is in use.
1368

1369
    """
1370
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "down"])
1371
    if result.failed:
1372
      logging.error("Can't shutdown drbd device: %s", result.output)
1373
    return not result.failed
1374

    
1375
  def Shutdown(self):
1376
    """Shutdown the DRBD device.
1377

1378
    """
1379
    if self.minor is None and not self.Attach():
1380
      logging.info("DRBD device not attached to a device during Shutdown")
1381
      return True
1382
    if not self._ShutdownAll(self.minor):
1383
      return False
1384
    self.minor = None
1385
    self.dev_path = None
1386
    return True
1387

    
1388
  def Remove(self):
1389
    """Stub remove for DRBD devices.
1390

1391
    """
1392
    return self.Shutdown()
1393

    
1394
  @classmethod
1395
  def Create(cls, unique_id, children, size):
1396
    """Create a new DRBD8 device.
1397

1398
    Since DRBD devices are not created per se, just assembled, this
1399
    function only initializes the metadata.
1400

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

    
1415
  def Grow(self, amount):
1416
    """Resize the DRBD device and its backing storage.
1417

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

    
1430

    
1431
class FileStorage(BlockDev):
1432
  """File device.
1433

1434
  This class represents the a file storage backend device.
1435

1436
  The unique_id for the file device is a (file_driver, file_path) tuple.
1437

1438
  """
1439
  def __init__(self, unique_id, children):
1440
    """Initalizes a file device backend.
1441

1442
    """
1443
    if children:
1444
      raise errors.BlockDeviceError("Invalid setup for file device")
1445
    super(FileStorage, self).__init__(unique_id, children)
1446
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1447
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1448
    self.driver = unique_id[0]
1449
    self.dev_path = unique_id[1]
1450

    
1451
  def Assemble(self):
1452
    """Assemble the device.
1453

1454
    Checks whether the file device exists, raises BlockDeviceError otherwise.
1455

1456
    """
1457
    if not os.path.exists(self.dev_path):
1458
      raise errors.BlockDeviceError("File device '%s' does not exist." %
1459
                                    self.dev_path)
1460
    return True
1461

    
1462
  def Shutdown(self):
1463
    """Shutdown the device.
1464

1465
    This is a no-op for the file type, as we don't deacivate
1466
    the file on shutdown.
1467

1468
    """
1469
    return True
1470

    
1471
  def Open(self, force=False):
1472
    """Make the device ready for I/O.
1473

1474
    This is a no-op for the file type.
1475

1476
    """
1477
    pass
1478

    
1479
  def Close(self):
1480
    """Notifies that the device will no longer be used for I/O.
1481

1482
    This is a no-op for the file type.
1483

1484
    """
1485
    pass
1486

    
1487
  def Remove(self):
1488
    """Remove the file backing the block device.
1489

1490
    Returns:
1491
      boolean indicating wheter removal of file was successful or not.
1492

1493
    """
1494
    if not os.path.exists(self.dev_path):
1495
      return True
1496
    try:
1497
      os.remove(self.dev_path)
1498
      return True
1499
    except OSError, err:
1500
      logging.error("Can't remove file '%s': %s", self.dev_path, err)
1501
      return False
1502

    
1503
  def Attach(self):
1504
    """Attach to an existing file.
1505

1506
    Check if this file already exists.
1507

1508
    Returns:
1509
      boolean indicating if file exists or not.
1510

1511
    """
1512
    if os.path.exists(self.dev_path):
1513
      return True
1514
    return False
1515

    
1516
  @classmethod
1517
  def Create(cls, unique_id, children, size):
1518
    """Create a new file.
1519

1520
    Args:
1521
      children:
1522
      size: integer size of file in MiB
1523

1524
    Returns:
1525
      A ganeti.bdev.FileStorage object.
1526

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

    
1539
    return FileStorage(unique_id, children)
1540

    
1541

    
1542
DEV_MAP = {
1543
  constants.LD_LV: LogicalVolume,
1544
  constants.LD_DRBD8: DRBD8,
1545
  constants.LD_FILE: FileStorage,
1546
  }
1547

    
1548

    
1549
def FindDevice(dev_type, unique_id, children):
1550
  """Search for an existing, assembled device.
1551

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

1555
  """
1556
  if dev_type not in DEV_MAP:
1557
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1558
  device = DEV_MAP[dev_type](unique_id, children)
1559
  if not device.attached:
1560
    return None
1561
  return  device
1562

    
1563

    
1564
def AttachOrAssemble(dev_type, unique_id, children):
1565
  """Try to attach or assemble an existing device.
1566

1567
  This will attach to an existing assembled device or will assemble
1568
  the device, as needed, to bring it fully up.
1569

1570
  """
1571
  if dev_type not in DEV_MAP:
1572
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1573
  device = DEV_MAP[dev_type](unique_id, children)
1574
  if not device.attached:
1575
    device.Assemble()
1576
    if not device.attached:
1577
      raise errors.BlockDeviceError("Can't find a valid block device for"
1578
                                    " %s/%s/%s" %
1579
                                    (dev_type, unique_id, children))
1580
  return device
1581

    
1582

    
1583
def Create(dev_type, unique_id, children, size):
1584
  """Create a device.
1585

1586
  """
1587
  if dev_type not in DEV_MAP:
1588
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1589
  device = DEV_MAP[dev_type].Create(unique_id, children, size)
1590
  return device