Statistics
| Branch: | Tag: | Revision:

root / lib / bdev.py @ ff9efc03

History | View | Annotate | Download (50.2 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) != 6:
808
      raise ValueError("Invalid configuration data %s" % str(unique_id))
809
    (self._lhost, self._lport,
810
     self._rhost, self._rport,
811
     self._aminor, self._secret) = unique_id
812
    if (self._lhost is not None and self._lhost == self._rhost and
813
        self._lport == self._rport):
814
      raise ValueError("Invalid configuration data, same local/remote %s" %
815
                       (unique_id,))
816
    self.Attach()
817

    
818
  @classmethod
819
  def _InitMeta(cls, minor, dev_path):
820
    """Initialize a meta device.
821

822
    This will not work if the given minor is in use.
823

824
    """
825
    result = utils.RunCmd(["drbdmeta", "--force", cls._DevPath(minor),
826
                           "v08", dev_path, "0", "create-md"])
827
    if result.failed:
828
      raise errors.BlockDeviceError("Can't initialize meta device: %s" %
829
                                    result.output)
830

    
831
  @classmethod
832
  def _FindUnusedMinor(cls):
833
    """Find an unused DRBD device.
834

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

838
    """
839
    data = cls._GetProcData()
840

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

    
859
  @classmethod
860
  def _IsValidMeta(cls, meta_device):
861
    """Check if the given meta device looks like a valid one.
862

863
    """
864
    minor = cls._FindUnusedMinor()
865
    minor_path = cls._DevPath(minor)
866
    result = utils.RunCmd(["drbdmeta", minor_path,
867
                           "v08", meta_device, "0",
868
                           "dstate"])
869
    if result.failed:
870
      logging.error("Invalid meta device %s: %s", meta_device, result.output)
871
      return False
872
    return True
873

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

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

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

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

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

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

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

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

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

    
917
    bnf = pyp.ZeroOrMore(pyp.Group(section ^ stmt))
918
    bnf.ignore(comment)
919

    
920
    cls._PARSE_SHOW = bnf
921

    
922
    return bnf
923

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

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

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

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

945
    """
946
    data = {}
947
    if not out:
948
      return data
949

    
950
    bnf = cls._GetShowParser()
951
    # run pyparse
952

    
953
    try:
954
      results = bnf.parseString(out)
955
    except pyp.ParseException, err:
956
      raise errors.BlockDeviceError("Can't parse drbdsetup show output: %s" %
957
                                    str(err))
958

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

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

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

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

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

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

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

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

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

    
1019
    if self._lhost is None:
1020
      return False
1021

    
1022
    if not ("local_addr" in info and
1023
            "remote_addr" in info):
1024
      return False
1025

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

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

1035
    This is the first thing that must be done on an unconfigured DRBD
1036
    device. And it must be done only once.
1037

1038
    """
1039
    if not cls._IsValidMeta(meta):
1040
      return False
1041
    args = ["drbdsetup", cls._DevPath(minor), "disk",
1042
            backend, meta, "0", "-e", "detach", "--create-device"]
1043
    result = utils.RunCmd(args)
1044
    if result.failed:
1045
      logging.error("Can't attach local disk: %s", result.output)
1046
    return not result.failed
1047

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

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

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

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

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

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

    
1116
    if not self._AssembleLocal(self.minor, backend.dev_path, meta.dev_path):
1117
      raise errors.BlockDeviceError("Can't attach to local storage")
1118
    self._children = devices
1119

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

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

    
1145
    if not self._ShutdownLocal(self.minor):
1146
      raise errors.BlockDeviceError("Can't detach from local storage")
1147
    self._children = []
1148

    
1149
  def SetSyncSpeed(self, kbytes):
1150
    """Set the speed of the DRBD syncer.
1151

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

    
1164
  def GetProcStatus(self):
1165
    """Return device data from /proc.
1166

1167
    """
1168
    if self.minor is None:
1169
      raise errors.BlockDeviceError("GetStats() called while not attached")
1170
    proc_info = self._MassageProcData(self._GetProcData())
1171
    if self.minor not in proc_info:
1172
      raise errors.BlockDeviceError("Can't find myself in /proc (minor %d)" %
1173
                                    self.minor)
1174
    return DRBD8Status(proc_info[self.minor])
1175

    
1176
  def GetSyncStatus(self):
1177
    """Returns the sync status of the device.
1178

1179
    Returns:
1180
     (sync_percent, estimated_time, is_degraded)
1181

1182
    If sync_percent is None, it means all is ok
1183
    If estimated_time is None, it means we can't esimate
1184
    the time needed, otherwise it's the time left in seconds.
1185

1186

1187
    We set the is_degraded parameter to True on two conditions:
1188
    network not connected or local disk missing.
1189

1190
    We compute the ldisk parameter based on wheter we have a local
1191
    disk or not.
1192

1193
    """
1194
    if self.minor is None and not self.Attach():
1195
      raise errors.BlockDeviceError("Can't attach to device in GetSyncStatus")
1196
    stats = self.GetProcStatus()
1197
    ldisk = not stats.is_disk_uptodate
1198
    is_degraded = not stats.is_connected
1199
    return stats.sync_percent, stats.est_time, is_degraded or ldisk, ldisk
1200

    
1201
  def Open(self, force=False):
1202
    """Make the local state primary.
1203

1204
    If the 'force' parameter is given, the '-o' option is passed to
1205
    drbdsetup. Since this is a potentially dangerous operation, the
1206
    force flag should be only given after creation, when it actually
1207
    is mandatory.
1208

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

    
1222
  def Close(self):
1223
    """Make the local state secondary.
1224

1225
    This will, of course, fail if the device is in use.
1226

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

    
1238
  def Attach(self):
1239
    """Find a DRBD device which matches our config and attach to it.
1240

1241
    In case of partially attached (local device matches but no network
1242
    setup), we perform the network attach. If successful, we re-test
1243
    the attach if can return success.
1244

1245
    """
1246
    for minor in (self._aminor,):
1247
      info = self._GetDevInfo(self._GetShowData(minor))
1248
      match_l = self._MatchesLocal(info)
1249
      match_r = self._MatchesNet(info)
1250
      if match_l and match_r:
1251
        break
1252
      if match_l and not match_r and "local_addr" not in info:
1253
        res_r = self._AssembleNet(minor,
1254
                                  (self._lhost, self._lport,
1255
                                   self._rhost, self._rport),
1256
                                  constants.DRBD_NET_PROTOCOL,
1257
                                  hmac=constants.DRBD_HMAC_ALG,
1258
                                  secret=self._secret
1259
                                  )
1260
        if res_r:
1261
          if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1262
            break
1263
      # the weakest case: we find something that is only net attached
1264
      # even though we were passed some children at init time
1265
      if match_r and "local_dev" not in info:
1266
        break
1267

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

    
1294
    else:
1295
      minor = None
1296

    
1297
    self._SetFromMinor(minor)
1298
    return minor is not None
1299

    
1300
  def Assemble(self):
1301
    """Assemble the drbd.
1302

1303
    Method:
1304
      - if we have a local backing device, we bind to it by:
1305
        - checking the list of used drbd devices
1306
        - check if the local minor use of any of them is our own device
1307
        - if yes, abort?
1308
        - if not, bind
1309
      - if we have a local/remote net info:
1310
        - redo the local backing device step for the remote device
1311
        - check if any drbd device is using the local port,
1312
          if yes abort
1313
        - check if any remote drbd device is using the remote
1314
          port, if yes abort (for now)
1315
        - bind our net port
1316
        - bind the remote net port
1317

1318
    """
1319
    self.Attach()
1320
    if self.minor is not None:
1321
      logging.info("Already assembled")
1322
      return True
1323

    
1324
    result = super(DRBD8, self).Assemble()
1325
    if not result:
1326
      return result
1327

    
1328
    # TODO: maybe completely tear-down the minor (drbdsetup ... down)
1329
    # before attaching our own?
1330
    minor = self._aminor
1331
    need_localdev_teardown = False
1332
    if self._children and self._children[0] and self._children[1]:
1333
      result = self._AssembleLocal(minor, self._children[0].dev_path,
1334
                                   self._children[1].dev_path)
1335
      if not result:
1336
        return False
1337
      need_localdev_teardown = True
1338
    if self._lhost and self._lport and self._rhost and self._rport:
1339
      result = self._AssembleNet(minor,
1340
                                 (self._lhost, self._lport,
1341
                                  self._rhost, self._rport),
1342
                                 constants.DRBD_NET_PROTOCOL,
1343
                                 hmac=constants.DRBD_HMAC_ALG,
1344
                                 secret=self._secret)
1345
      if not result:
1346
        if need_localdev_teardown:
1347
          # we will ignore failures from this
1348
          logging.error("net setup failed, tearing down local device")
1349
          self._ShutdownAll(minor)
1350
        return False
1351
    self._SetFromMinor(minor)
1352
    return True
1353

    
1354
  @classmethod
1355
  def _ShutdownLocal(cls, minor):
1356
    """Detach from the local device.
1357

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

1361
    """
1362
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "detach"])
1363
    if result.failed:
1364
      logging.error("Can't detach local device: %s", result.output)
1365
    return not result.failed
1366

    
1367
  @classmethod
1368
  def _ShutdownNet(cls, minor):
1369
    """Disconnect from the remote peer.
1370

1371
    This fails if we don't have a local device.
1372

1373
    """
1374
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disconnect"])
1375
    if result.failed:
1376
      logging.error("Can't shutdown network: %s", result.output)
1377
    return not result.failed
1378

    
1379
  @classmethod
1380
  def _ShutdownAll(cls, minor):
1381
    """Deactivate the device.
1382

1383
    This will, of course, fail if the device is in use.
1384

1385
    """
1386
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "down"])
1387
    if result.failed:
1388
      logging.error("Can't shutdown drbd device: %s", result.output)
1389
    return not result.failed
1390

    
1391
  def Shutdown(self):
1392
    """Shutdown the DRBD device.
1393

1394
    """
1395
    if self.minor is None and not self.Attach():
1396
      logging.info("DRBD device not attached to a device during Shutdown")
1397
      return True
1398
    if not self._ShutdownAll(self.minor):
1399
      return False
1400
    self.minor = None
1401
    self.dev_path = None
1402
    return True
1403

    
1404
  def Remove(self):
1405
    """Stub remove for DRBD devices.
1406

1407
    """
1408
    return self.Shutdown()
1409

    
1410
  @classmethod
1411
  def Create(cls, unique_id, children, size):
1412
    """Create a new DRBD8 device.
1413

1414
    Since DRBD devices are not created per se, just assembled, this
1415
    function only initializes the metadata.
1416

1417
    """
1418
    if len(children) != 2:
1419
      raise errors.ProgrammerError("Invalid setup for the drbd device")
1420
    meta = children[1]
1421
    meta.Assemble()
1422
    if not meta.Attach():
1423
      raise errors.BlockDeviceError("Can't attach to meta device")
1424
    if not cls._CheckMetaSize(meta.dev_path):
1425
      raise errors.BlockDeviceError("Invalid meta device size")
1426
    cls._InitMeta(cls._FindUnusedMinor(), meta.dev_path)
1427
    if not cls._IsValidMeta(meta.dev_path):
1428
      raise errors.BlockDeviceError("Cannot initalize meta device")
1429
    return cls(unique_id, children)
1430

    
1431
  def Grow(self, amount):
1432
    """Resize the DRBD device and its backing storage.
1433

1434
    """
1435
    if self.minor is None:
1436
      raise errors.ProgrammerError("drbd8: Grow called while not attached")
1437
    if len(self._children) != 2 or None in self._children:
1438
      raise errors.BlockDeviceError("Cannot grow diskless DRBD8 device")
1439
    self._children[0].Grow(amount)
1440
    result = utils.RunCmd(["drbdsetup", self.dev_path, "resize"])
1441
    if result.failed:
1442
      raise errors.BlockDeviceError("resize failed for %s: %s" %
1443
                                    (self.dev_path, result.output))
1444
    return
1445

    
1446

    
1447
class FileStorage(BlockDev):
1448
  """File device.
1449

1450
  This class represents the a file storage backend device.
1451

1452
  The unique_id for the file device is a (file_driver, file_path) tuple.
1453

1454
  """
1455
  def __init__(self, unique_id, children):
1456
    """Initalizes a file device backend.
1457

1458
    """
1459
    if children:
1460
      raise errors.BlockDeviceError("Invalid setup for file device")
1461
    super(FileStorage, self).__init__(unique_id, children)
1462
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1463
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1464
    self.driver = unique_id[0]
1465
    self.dev_path = unique_id[1]
1466
    self.Attach()
1467

    
1468
  def Assemble(self):
1469
    """Assemble the device.
1470

1471
    Checks whether the file device exists, raises BlockDeviceError otherwise.
1472

1473
    """
1474
    if not os.path.exists(self.dev_path):
1475
      raise errors.BlockDeviceError("File device '%s' does not exist." %
1476
                                    self.dev_path)
1477
    return True
1478

    
1479
  def Shutdown(self):
1480
    """Shutdown the device.
1481

1482
    This is a no-op for the file type, as we don't deacivate
1483
    the file on shutdown.
1484

1485
    """
1486
    return True
1487

    
1488
  def Open(self, force=False):
1489
    """Make the device ready for I/O.
1490

1491
    This is a no-op for the file type.
1492

1493
    """
1494
    pass
1495

    
1496
  def Close(self):
1497
    """Notifies that the device will no longer be used for I/O.
1498

1499
    This is a no-op for the file type.
1500

1501
    """
1502
    pass
1503

    
1504
  def Remove(self):
1505
    """Remove the file backing the block device.
1506

1507
    Returns:
1508
      boolean indicating wheter removal of file was successful or not.
1509

1510
    """
1511
    if not os.path.exists(self.dev_path):
1512
      return True
1513
    try:
1514
      os.remove(self.dev_path)
1515
      return True
1516
    except OSError, err:
1517
      logging.error("Can't remove file '%s': %s", self.dev_path, err)
1518
      return False
1519

    
1520
  def Attach(self):
1521
    """Attach to an existing file.
1522

1523
    Check if this file already exists.
1524

1525
    Returns:
1526
      boolean indicating if file exists or not.
1527

1528
    """
1529
    self.attached = os.path.exists(self.dev_path)
1530
    return self.attached
1531

    
1532
  @classmethod
1533
  def Create(cls, unique_id, children, size):
1534
    """Create a new file.
1535

1536
    Args:
1537
      children:
1538
      size: integer size of file in MiB
1539

1540
    Returns:
1541
      A ganeti.bdev.FileStorage object.
1542

1543
    """
1544
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1545
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1546
    dev_path = unique_id[1]
1547
    try:
1548
      f = open(dev_path, 'w')
1549
    except IOError, err:
1550
      raise errors.BlockDeviceError("Could not create '%'" % err)
1551
    else:
1552
      f.truncate(size * 1024 * 1024)
1553
      f.close()
1554

    
1555
    return FileStorage(unique_id, children)
1556

    
1557

    
1558
DEV_MAP = {
1559
  constants.LD_LV: LogicalVolume,
1560
  constants.LD_DRBD8: DRBD8,
1561
  constants.LD_FILE: FileStorage,
1562
  }
1563

    
1564

    
1565
def FindDevice(dev_type, unique_id, children):
1566
  """Search for an existing, assembled device.
1567

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

1571
  """
1572
  if dev_type not in DEV_MAP:
1573
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1574
  device = DEV_MAP[dev_type](unique_id, children)
1575
  if not device.attached:
1576
    return None
1577
  return device
1578

    
1579

    
1580
def AttachOrAssemble(dev_type, unique_id, children):
1581
  """Try to attach or assemble an existing device.
1582

1583
  This will attach to an existing assembled device or will assemble
1584
  the device, as needed, to bring it fully up.
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](unique_id, children)
1590
  if not device.attached:
1591
    device.Assemble()
1592
    if not device.attached:
1593
      raise errors.BlockDeviceError("Can't find a valid block device for"
1594
                                    " %s/%s/%s" %
1595
                                    (dev_type, unique_id, children))
1596
  return device
1597

    
1598

    
1599
def Create(dev_type, unique_id, children, size):
1600
  """Create a device.
1601

1602
  """
1603
  if dev_type not in DEV_MAP:
1604
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1605
  device = DEV_MAP[dev_type].Create(unique_id, children, size)
1606
  return device