Statistics
| Branch: | Tag: | Revision:

root / lib / bdev.py @ cf8df3f3

History | View | Annotate | Download (53.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
    If sync_percent is None, it means the device is not syncing.
206

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

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

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

218
    @rtype: tuple
219
    @return: (sync_percent, estimated_time, is_degraded, ldisk)
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
    @param amount: the amount (in mebibytes) to grow with
263

264
    """
265
    raise NotImplementedError
266

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

    
272

    
273
class LogicalVolume(BlockDev):
274
  """Logical Volume block device.
275

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

280
    The unique_id is a tuple (vg_name, lv_name)
281

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

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

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

    
307
    pvlist = [ pv[1] for pv in pvs_info ]
308
    free_size = sum([ pv[0] for pv in pvs_info ])
309

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

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

326
    @param vg_name: the volume group name
327

328
    @rtype: list
329
    @return: list of tuples (free_space, name) with free_space in mebibytes
330

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

    
351
    return data
352

    
353
  def Remove(self):
354
    """Remove this logical volume.
355

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

    
366
    return not result.failed
367

    
368
  def Rename(self, new_id):
369
    """Rename this logical volume.
370

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

    
386
  def Attach(self):
387
    """Attach to an existing LV.
388

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

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

    
408
    status, major, minor = out[:3]
409
    if len(status) != 6:
410
      logging.error("lvs lv_attr is not 6 characters (%s)", status)
411
      return False
412

    
413
    try:
414
      major = int(major)
415
      minor = int(minor)
416
    except ValueError, err:
417
      logging.error("lvs major/minor cannot be parsed: %s", str(err))
418

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

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

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

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

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

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

446
    """
447
    return True
448

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

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

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

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

466
    The status was already read in Attach, so we just return it.
467

468
    @rtype: tuple
469
    @return: (sync_percent, estimated_time, is_degraded, ldisk)
470

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

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

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

479
    """
480
    pass
481

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

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

487
    """
488
    pass
489

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

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

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

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

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

    
518
    return snap_name
519

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

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

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

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

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

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

    
555

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

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

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

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

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

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

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

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

    
605

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

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

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

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

    
621
  _STATUS_FILE = "/proc/drbd"
622

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

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

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

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

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

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

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

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

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

    
693
    return retval
694

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

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

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

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

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

    
721
    return used_devs
722

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

726
    This sets our minor variable and our dev_path.
727

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

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

741
    This currently only check the size, which must be around
742
    128MiB.
743

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

    
764
  def Rename(self, new_id):
765
    """Rename a device.
766

767
    This is not supported for drbd devices.
768

769
    """
770
    raise errors.ProgrammerError("Can't rename a drbd device")
771

    
772

    
773
class DRBD8(BaseDRBD):
774
  """DRBD v8.x block device.
775

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

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

785
  """
786
  _MAX_MINORS = 255
787
  _PARSE_SHOW = None
788

    
789
  # timeout constants
790
  _NET_RECONFIG_TIMEOUT = 60
791

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

    
804
    if len(children) not in (0, 2):
805
      raise ValueError("Invalid configuration data %s" % str(children))
806
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 6:
807
      raise ValueError("Invalid configuration data %s" % str(unique_id))
808
    (self._lhost, self._lport,
809
     self._rhost, self._rport,
810
     self._aminor, self._secret) = unique_id
811
    if (self._lhost is not None and self._lhost == self._rhost and
812
        self._lport == self._rport):
813
      raise ValueError("Invalid configuration data, same local/remote %s" %
814
                       (unique_id,))
815
    self.Attach()
816

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
919
    cls._PARSE_SHOW = bnf
920

    
921
    return bnf
922

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1178

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

1183

1184
    We set the is_degraded parameter to True on two conditions:
1185
    network not connected or local disk missing.
1186

1187
    We compute the ldisk parameter based on wheter we have a local
1188
    disk or not.
1189

1190
    @rtype: tuple
1191
    @return: (sync_percent, estimated_time, is_degraded, ldisk)
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 DisconnectNet(self):
1239
    """Removes network configuration.
1240

1241
    This method shutdowns the network side of the device.
1242

1243
    The method will wait up to a hardcoded timeout for the device to
1244
    go into standalone after the 'disconnect' command before
1245
    re-configuring it, as sometimes it takes a while for the
1246
    disconnect to actually propagate and thus we might issue a 'net'
1247
    command while the device is still connected. If the device will
1248
    still be attached to the network and we time out, we raise an
1249
    exception.
1250

1251
    """
1252
    if self.minor is None:
1253
      raise errors.BlockDeviceError("DRBD disk not attached in re-attach net")
1254

    
1255
    if None in (self._lhost, self._lport, self._rhost, self._rport):
1256
      raise errors.BlockDeviceError("DRBD disk missing network info in"
1257
                                    " DisconnectNet()")
1258

    
1259
    ever_disconnected = self._ShutdownNet(self.minor)
1260
    timeout_limit = time.time() + self._NET_RECONFIG_TIMEOUT
1261
    sleep_time = 0.100 # we start the retry time at 100 miliseconds
1262
    while time.time() < timeout_limit:
1263
      status = self.GetProcStatus()
1264
      if status.is_standalone:
1265
        break
1266
      # retry the disconnect, it seems possible that due to a
1267
      # well-time disconnect on the peer, my disconnect command might
1268
      # be ingored and forgotten
1269
      ever_disconnected = self._ShutdownNet(self.minor) or ever_disconnected
1270
      time.sleep(sleep_time)
1271
      sleep_time = min(2, sleep_time * 1.5)
1272

    
1273
    if not status.is_standalone:
1274
      if ever_disconnected:
1275
        msg = ("Device did not react to the"
1276
               " 'disconnect' command in a timely manner")
1277
      else:
1278
        msg = ("Can't shutdown network, even after multiple retries")
1279
      raise errors.BlockDeviceError(msg)
1280

    
1281
    reconfig_time = time.time() - timeout_limit + self._NET_RECONFIG_TIMEOUT
1282
    if reconfig_time > 15: # hardcoded alert limit
1283
      logging.debug("DRBD8.DisconnectNet: detach took %.3f seconds",
1284
                    reconfig_time)
1285

    
1286
  def AttachNet(self, multimaster):
1287
    """Reconnects the network.
1288

1289
    This method connects the network side of the device with a
1290
    specified multi-master flag. The device needs to be 'Standalone'
1291
    but have valid network configuration data.
1292

1293
    Args:
1294
      - multimaster: init the network in dual-primary mode
1295

1296
    """
1297
    if self.minor is None:
1298
      raise errors.BlockDeviceError("DRBD disk not attached in AttachNet")
1299

    
1300
    if None in (self._lhost, self._lport, self._rhost, self._rport):
1301
      raise errors.BlockDeviceError("DRBD disk missing network info in"
1302
                                    " AttachNet()")
1303

    
1304
    status = self.GetProcStatus()
1305

    
1306
    if not status.is_standalone:
1307
      raise errors.BlockDeviceError("Device is not standalone in AttachNet")
1308

    
1309
    return self._AssembleNet(self.minor,
1310
                             (self._lhost, self._lport,
1311
                              self._rhost, self._rport),
1312
                             "C", dual_pri=multimaster)
1313

    
1314
  def Attach(self):
1315
    """Find a DRBD device which matches our config and attach to it.
1316

1317
    In case of partially attached (local device matches but no network
1318
    setup), we perform the network attach. If successful, we re-test
1319
    the attach if can return success.
1320

1321
    """
1322
    for minor in (self._aminor,):
1323
      info = self._GetDevInfo(self._GetShowData(minor))
1324
      match_l = self._MatchesLocal(info)
1325
      match_r = self._MatchesNet(info)
1326
      if match_l and match_r:
1327
        break
1328
      if match_l and not match_r and "local_addr" not in info:
1329
        res_r = self._AssembleNet(minor,
1330
                                  (self._lhost, self._lport,
1331
                                   self._rhost, self._rport),
1332
                                  constants.DRBD_NET_PROTOCOL,
1333
                                  hmac=constants.DRBD_HMAC_ALG,
1334
                                  secret=self._secret
1335
                                  )
1336
        if res_r:
1337
          if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1338
            break
1339
      # the weakest case: we find something that is only net attached
1340
      # even though we were passed some children at init time
1341
      if match_r and "local_dev" not in info:
1342
        break
1343

    
1344
      # this case must be considered only if we actually have local
1345
      # storage, i.e. not in diskless mode, because all diskless
1346
      # devices are equal from the point of view of local
1347
      # configuration
1348
      if (match_l and "local_dev" in info and
1349
          not match_r and "local_addr" in info):
1350
        # strange case - the device network part points to somewhere
1351
        # else, even though its local storage is ours; as we own the
1352
        # drbd space, we try to disconnect from the remote peer and
1353
        # reconnect to our correct one
1354
        if not self._ShutdownNet(minor):
1355
          raise errors.BlockDeviceError("Device has correct local storage,"
1356
                                        " wrong remote peer and is unable to"
1357
                                        " disconnect in order to attach to"
1358
                                        " the correct peer")
1359
        # note: _AssembleNet also handles the case when we don't want
1360
        # local storage (i.e. one or more of the _[lr](host|port) is
1361
        # None)
1362
        if (self._AssembleNet(minor, (self._lhost, self._lport,
1363
                                      self._rhost, self._rport),
1364
                              constants.DRBD_NET_PROTOCOL,
1365
                              hmac=constants.DRBD_HMAC_ALG,
1366
                              secret=self._secret) and
1367
            self._MatchesNet(self._GetDevInfo(self._GetShowData(minor)))):
1368
          break
1369

    
1370
    else:
1371
      minor = None
1372

    
1373
    self._SetFromMinor(minor)
1374
    return minor is not None
1375

    
1376
  def Assemble(self):
1377
    """Assemble the drbd.
1378

1379
    Method:
1380
      - if we have a local backing device, we bind to it by:
1381
        - checking the list of used drbd devices
1382
        - check if the local minor use of any of them is our own device
1383
        - if yes, abort?
1384
        - if not, bind
1385
      - if we have a local/remote net info:
1386
        - redo the local backing device step for the remote device
1387
        - check if any drbd device is using the local port,
1388
          if yes abort
1389
        - check if any remote drbd device is using the remote
1390
          port, if yes abort (for now)
1391
        - bind our net port
1392
        - bind the remote net port
1393

1394
    """
1395
    self.Attach()
1396
    if self.minor is not None:
1397
      logging.info("Already assembled")
1398
      return True
1399

    
1400
    result = super(DRBD8, self).Assemble()
1401
    if not result:
1402
      return result
1403

    
1404
    # TODO: maybe completely tear-down the minor (drbdsetup ... down)
1405
    # before attaching our own?
1406
    minor = self._aminor
1407
    need_localdev_teardown = False
1408
    if self._children and self._children[0] and self._children[1]:
1409
      result = self._AssembleLocal(minor, self._children[0].dev_path,
1410
                                   self._children[1].dev_path)
1411
      if not result:
1412
        return False
1413
      need_localdev_teardown = True
1414
    if self._lhost and self._lport and self._rhost and self._rport:
1415
      result = self._AssembleNet(minor,
1416
                                 (self._lhost, self._lport,
1417
                                  self._rhost, self._rport),
1418
                                 constants.DRBD_NET_PROTOCOL,
1419
                                 hmac=constants.DRBD_HMAC_ALG,
1420
                                 secret=self._secret)
1421
      if not result:
1422
        if need_localdev_teardown:
1423
          # we will ignore failures from this
1424
          logging.error("net setup failed, tearing down local device")
1425
          self._ShutdownAll(minor)
1426
        return False
1427
    self._SetFromMinor(minor)
1428
    return True
1429

    
1430
  @classmethod
1431
  def _ShutdownLocal(cls, minor):
1432
    """Detach from the local device.
1433

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

1437
    """
1438
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "detach"])
1439
    if result.failed:
1440
      logging.error("Can't detach local device: %s", result.output)
1441
    return not result.failed
1442

    
1443
  @classmethod
1444
  def _ShutdownNet(cls, minor):
1445
    """Disconnect from the remote peer.
1446

1447
    This fails if we don't have a local device.
1448

1449
    """
1450
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disconnect"])
1451
    if result.failed:
1452
      logging.error("Can't shutdown network: %s", result.output)
1453
    return not result.failed
1454

    
1455
  @classmethod
1456
  def _ShutdownAll(cls, minor):
1457
    """Deactivate the device.
1458

1459
    This will, of course, fail if the device is in use.
1460

1461
    """
1462
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "down"])
1463
    if result.failed:
1464
      logging.error("Can't shutdown drbd device: %s", result.output)
1465
    return not result.failed
1466

    
1467
  def Shutdown(self):
1468
    """Shutdown the DRBD device.
1469

1470
    """
1471
    if self.minor is None and not self.Attach():
1472
      logging.info("DRBD device not attached to a device during Shutdown")
1473
      return True
1474
    if not self._ShutdownAll(self.minor):
1475
      return False
1476
    self.minor = None
1477
    self.dev_path = None
1478
    return True
1479

    
1480
  def Remove(self):
1481
    """Stub remove for DRBD devices.
1482

1483
    """
1484
    return self.Shutdown()
1485

    
1486
  @classmethod
1487
  def Create(cls, unique_id, children, size):
1488
    """Create a new DRBD8 device.
1489

1490
    Since DRBD devices are not created per se, just assembled, this
1491
    function only initializes the metadata.
1492

1493
    """
1494
    if len(children) != 2:
1495
      raise errors.ProgrammerError("Invalid setup for the drbd device")
1496
    meta = children[1]
1497
    meta.Assemble()
1498
    if not meta.Attach():
1499
      raise errors.BlockDeviceError("Can't attach to meta device")
1500
    if not cls._CheckMetaSize(meta.dev_path):
1501
      raise errors.BlockDeviceError("Invalid meta device size")
1502
    cls._InitMeta(cls._FindUnusedMinor(), meta.dev_path)
1503
    if not cls._IsValidMeta(meta.dev_path):
1504
      raise errors.BlockDeviceError("Cannot initalize meta device")
1505
    return cls(unique_id, children)
1506

    
1507
  def Grow(self, amount):
1508
    """Resize the DRBD device and its backing storage.
1509

1510
    """
1511
    if self.minor is None:
1512
      raise errors.ProgrammerError("drbd8: Grow called while not attached")
1513
    if len(self._children) != 2 or None in self._children:
1514
      raise errors.BlockDeviceError("Cannot grow diskless DRBD8 device")
1515
    self._children[0].Grow(amount)
1516
    result = utils.RunCmd(["drbdsetup", self.dev_path, "resize"])
1517
    if result.failed:
1518
      raise errors.BlockDeviceError("resize failed for %s: %s" %
1519
                                    (self.dev_path, result.output))
1520
    return
1521

    
1522

    
1523
class FileStorage(BlockDev):
1524
  """File device.
1525

1526
  This class represents the a file storage backend device.
1527

1528
  The unique_id for the file device is a (file_driver, file_path) tuple.
1529

1530
  """
1531
  def __init__(self, unique_id, children):
1532
    """Initalizes a file device backend.
1533

1534
    """
1535
    if children:
1536
      raise errors.BlockDeviceError("Invalid setup for file device")
1537
    super(FileStorage, self).__init__(unique_id, children)
1538
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1539
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1540
    self.driver = unique_id[0]
1541
    self.dev_path = unique_id[1]
1542
    self.Attach()
1543

    
1544
  def Assemble(self):
1545
    """Assemble the device.
1546

1547
    Checks whether the file device exists, raises BlockDeviceError otherwise.
1548

1549
    """
1550
    if not os.path.exists(self.dev_path):
1551
      raise errors.BlockDeviceError("File device '%s' does not exist." %
1552
                                    self.dev_path)
1553
    return True
1554

    
1555
  def Shutdown(self):
1556
    """Shutdown the device.
1557

1558
    This is a no-op for the file type, as we don't deacivate
1559
    the file on shutdown.
1560

1561
    """
1562
    return True
1563

    
1564
  def Open(self, force=False):
1565
    """Make the device ready for I/O.
1566

1567
    This is a no-op for the file type.
1568

1569
    """
1570
    pass
1571

    
1572
  def Close(self):
1573
    """Notifies that the device will no longer be used for I/O.
1574

1575
    This is a no-op for the file type.
1576

1577
    """
1578
    pass
1579

    
1580
  def Remove(self):
1581
    """Remove the file backing the block device.
1582

1583
    @rtype: boolean
1584
    @return: True if the removal was successful
1585

1586
    """
1587
    if not os.path.exists(self.dev_path):
1588
      return True
1589
    try:
1590
      os.remove(self.dev_path)
1591
      return True
1592
    except OSError, err:
1593
      logging.error("Can't remove file '%s': %s", self.dev_path, err)
1594
      return False
1595

    
1596
  def Attach(self):
1597
    """Attach to an existing file.
1598

1599
    Check if this file already exists.
1600

1601
    @rtype: boolean
1602
    @return: True if file exists
1603

1604
    """
1605
    self.attached = os.path.exists(self.dev_path)
1606
    return self.attached
1607

    
1608
  @classmethod
1609
  def Create(cls, unique_id, children, size):
1610
    """Create a new file.
1611

1612
    @param size: the size of file in MiB
1613

1614
    @rtype: L{bdev.FileStorage}
1615
    @return: an instance of FileStorage
1616

1617
    """
1618
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1619
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1620
    dev_path = unique_id[1]
1621
    try:
1622
      f = open(dev_path, 'w')
1623
    except IOError, err:
1624
      raise errors.BlockDeviceError("Could not create '%'" % err)
1625
    else:
1626
      f.truncate(size * 1024 * 1024)
1627
      f.close()
1628

    
1629
    return FileStorage(unique_id, children)
1630

    
1631

    
1632
DEV_MAP = {
1633
  constants.LD_LV: LogicalVolume,
1634
  constants.LD_DRBD8: DRBD8,
1635
  constants.LD_FILE: FileStorage,
1636
  }
1637

    
1638

    
1639
def FindDevice(dev_type, unique_id, children):
1640
  """Search for an existing, assembled device.
1641

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

1645
  """
1646
  if dev_type not in DEV_MAP:
1647
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1648
  device = DEV_MAP[dev_type](unique_id, children)
1649
  if not device.attached:
1650
    return None
1651
  return device
1652

    
1653

    
1654
def AttachOrAssemble(dev_type, unique_id, children):
1655
  """Try to attach or assemble an existing device.
1656

1657
  This will attach to an existing assembled device or will assemble
1658
  the device, as needed, to bring it fully up.
1659

1660
  """
1661
  if dev_type not in DEV_MAP:
1662
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1663
  device = DEV_MAP[dev_type](unique_id, children)
1664
  if not device.attached:
1665
    device.Assemble()
1666
    if not device.attached:
1667
      raise errors.BlockDeviceError("Can't find a valid block device for"
1668
                                    " %s/%s/%s" %
1669
                                    (dev_type, unique_id, children))
1670
  return device
1671

    
1672

    
1673
def Create(dev_type, unique_id, children, size):
1674
  """Create a device.
1675

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