Statistics
| Branch: | Tag: | Revision:

root / lib / bdev.py @ 742f39ac

History | View | Annotate | Download (73 kB)

1
#
2
#
3

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

    
21

    
22
"""Block device abstraction"""
23

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

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

    
35

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

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

45
  A device can also be online but read-only, however we are not using
46
  the readonly state (MD and LV have it, if needed in the future)
47
  and we are usually looking at this like at a stack, so it's easier
48
  to 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
    - md arrays are created or assembled and used
55
    - drbd devices are attached to a local disk/remote peer and made primary
56

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

62
  Not all devices implement both the first two as distinct items. LVM
63
  logical volumes have their unique ID (the pair volume group, logical
64
  volume name) in a 1-to-1 relation to the dev path. For MD devices,
65
  the /dev path is dynamic and the unique ID is the UUID generated at
66
  array creation plus the slave list. For DRBD devices, the /dev path
67
  is again dynamic and the unique id is the pair (host1, dev1),
68
  (host2, dev2).
69

70
  You can get to a device in two ways:
71
    - creating the (real) device, which returns you
72
      an attached instance (lvcreate, mdadm --create)
73
    - attaching of a python instance to an existing (real) device
74

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

81
  """
82
  def __init__(self, unique_id, children):
83
    self._children = children
84
    self.dev_path = None
85
    self.unique_id = unique_id
86
    self.major = None
87
    self.minor = None
88

    
89
  def Assemble(self):
90
    """Assemble the device from its components.
91

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

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

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

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

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

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

126
    """
127
    raise NotImplementedError
128

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

132
    """
133
    raise NotImplementedError
134

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

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

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

146
    """
147
    raise NotImplementedError
148

    
149
  def Remove(self):
150
    """Remove this device.
151

152
    This makes sense only for some of the device types: LV and to a
153
    lesser degree, md devices. Also note that if the device can't
154
    attach, the removal can't be completed.
155

156
    """
157
    raise NotImplementedError
158

    
159
  def Rename(self, new_id):
160
    """Rename this device.
161

162
    This may or may not make sense for a given device type.
163

164
    """
165
    raise NotImplementedError
166

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

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

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

176
    """
177
    raise NotImplementedError
178

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

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

186
    """
187
    raise NotImplementedError
188

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

192
    In case this is not a mirroring device, this is no-op.
193

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

    
201
  def GetSyncStatus(self):
202
    """Returns the sync status of the device.
203

204
    If this device is a mirroring device, this function returns the
205
    status of the mirror.
206

207
    Returns:
208
     (sync_percent, estimated_time, is_degraded, ldisk)
209

210
    If sync_percent is None, it means the device is not syncing.
211

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

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

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

223
    """
224
    return None, None, False, False
225

    
226

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

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

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

    
251

    
252
  def SetInfo(self, text):
253
    """Update metadata with info text.
254

255
    Only supported for some device types.
256

257
    """
258
    for child in self._children:
259
      child.SetInfo(text)
260

    
261

    
262
  def __repr__(self):
263
    return ("<%s: unique_id: %s, children: %s, %s:%s, %s>" %
264
            (self.__class__, self.unique_id, self._children,
265
             self.major, self.minor, self.dev_path))
266

    
267

    
268
class LogicalVolume(BlockDev):
269
  """Logical Volume block device.
270

271
  """
272
  def __init__(self, unique_id, children):
273
    """Attaches to a LV device.
274

275
    The unique_id is a tuple (vg_name, lv_name)
276

277
    """
278
    super(LogicalVolume, self).__init__(unique_id, children)
279
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
280
      raise ValueError("Invalid configuration data %s" % str(unique_id))
281
    self._vg_name, self._lv_name = unique_id
282
    self.dev_path = "/dev/%s/%s" % (self._vg_name, self._lv_name)
283
    self.Attach()
284

    
285
  @classmethod
286
  def Create(cls, unique_id, children, size):
287
    """Create a new logical volume.
288

289
    """
290
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
291
      raise ValueError("Invalid configuration data %s" % str(unique_id))
292
    vg_name, lv_name = unique_id
293
    pvs_info = cls.GetPVInfo(vg_name)
294
    if not pvs_info:
295
      raise errors.BlockDeviceError("Can't compute PV info for vg %s" %
296
                                    vg_name)
297
    pvs_info.sort()
298
    pvs_info.reverse()
299

    
300
    pvlist = [ pv[1] for pv in pvs_info ]
301
    free_size = sum([ pv[0] for pv in pvs_info ])
302

    
303
    # The size constraint should have been checked from the master before
304
    # calling the create function.
305
    if free_size < size:
306
      raise errors.BlockDeviceError("Not enough free space: required %s,"
307
                                    " available %s" % (size, free_size))
308
    result = utils.RunCmd(["lvcreate", "-L%dm" % size, "-n%s" % lv_name,
309
                           vg_name] + pvlist)
310
    if result.failed:
311
      raise errors.BlockDeviceError(result.fail_reason)
312
    return LogicalVolume(unique_id, children)
313

    
314
  @staticmethod
315
  def GetPVInfo(vg_name):
316
    """Get the free space info for PVs in a volume group.
317

318
    Args:
319
      vg_name: the volume group name
320

321
    Returns:
322
      list of (free_space, name) with free_space in mebibytes
323

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

    
343
    return data
344

    
345
  def Remove(self):
346
    """Remove this logical volume.
347

348
    """
349
    if not self.minor and not self.Attach():
350
      # the LV does not exist
351
      return True
352
    result = utils.RunCmd(["lvremove", "-f", "%s/%s" %
353
                           (self._vg_name, self._lv_name)])
354
    if result.failed:
355
      logger.Error("Can't lvremove: %s" % result.fail_reason)
356

    
357
    return not result.failed
358

    
359
  def Rename(self, new_id):
360
    """Rename this logical volume.
361

362
    """
363
    if not isinstance(new_id, (tuple, list)) or len(new_id) != 2:
364
      raise errors.ProgrammerError("Invalid new logical id '%s'" % new_id)
365
    new_vg, new_name = new_id
366
    if new_vg != self._vg_name:
367
      raise errors.ProgrammerError("Can't move a logical volume across"
368
                                   " volume groups (from %s to to %s)" %
369
                                   (self._vg_name, new_vg))
370
    result = utils.RunCmd(["lvrename", new_vg, self._lv_name, new_name])
371
    if result.failed:
372
      raise errors.BlockDeviceError("Failed to rename the logical volume: %s" %
373
                                    result.output)
374
    self._lv_name = new_name
375
    self.dev_path = "/dev/%s/%s" % (self._vg_name, self._lv_name)
376

    
377
  def Attach(self):
378
    """Attach to an existing LV.
379

380
    This method will try to see if an existing and active LV exists
381
    which matches our name. If so, its major/minor will be
382
    recorded.
383

384
    """
385
    result = utils.RunCmd(["lvdisplay", self.dev_path])
386
    if result.failed:
387
      logger.Error("Can't find LV %s: %s, %s" %
388
                   (self.dev_path, result.fail_reason, result.output))
389
      return False
390
    match = re.compile("^ *Block device *([0-9]+):([0-9]+).*$")
391
    for line in result.stdout.splitlines():
392
      match_result = match.match(line)
393
      if match_result:
394
        self.major = int(match_result.group(1))
395
        self.minor = int(match_result.group(2))
396
        return True
397
    return False
398

    
399
  def Assemble(self):
400
    """Assemble the device.
401

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

406
    """
407
    result = utils.RunCmd(["lvchange", "-ay", self.dev_path])
408
    if result.failed:
409
      logger.Error("Can't activate lv %s: %s" % (self.dev_path, result.output))
410
    return not result.failed
411

    
412
  def Shutdown(self):
413
    """Shutdown the device.
414

415
    This is a no-op for the LV device type, as we don't deactivate the
416
    volumes on shutdown.
417

418
    """
419
    return True
420

    
421
  def GetSyncStatus(self):
422
    """Returns the sync status of the device.
423

424
    If this device is a mirroring device, this function returns the
425
    status of the mirror.
426

427
    Returns:
428
     (sync_percent, estimated_time, is_degraded, ldisk)
429

430
    For logical volumes, sync_percent and estimated_time are always
431
    None (no recovery in progress, as we don't handle the mirrored LV
432
    case). The is_degraded parameter is the inverse of the ldisk
433
    parameter.
434

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

441
    """
442
    result = utils.RunCmd(["lvs", "--noheadings", "-olv_attr", self.dev_path])
443
    if result.failed:
444
      logger.Error("Can't display lv: %s" % result.fail_reason)
445
      return None, None, True, True
446
    out = result.stdout.strip()
447
    # format: type/permissions/alloc/fixed_minor/state/open
448
    if len(out) != 6:
449
      logger.Debug("Error in lvs output: attrs=%s, len != 6" % out)
450
      return None, None, True, True
451
    ldisk = out[0] == 'v' # virtual volume, i.e. doesn't have
452
                          # backing storage
453
    return None, None, ldisk, ldisk
454

    
455
  def Open(self, force=False):
456
    """Make the device ready for I/O.
457

458
    This is a no-op for the LV device type.
459

460
    """
461
    pass
462

    
463
  def Close(self):
464
    """Notifies that the device will no longer be used for I/O.
465

466
    This is a no-op for the LV device type.
467

468
    """
469
    pass
470

    
471
  def Snapshot(self, size):
472
    """Create a snapshot copy of an lvm block device.
473

474
    """
475
    snap_name = self._lv_name + ".snap"
476

    
477
    # remove existing snapshot if found
478
    snap = LogicalVolume((self._vg_name, snap_name), None)
479
    snap.Remove()
480

    
481
    pvs_info = self.GetPVInfo(self._vg_name)
482
    if not pvs_info:
483
      raise errors.BlockDeviceError("Can't compute PV info for vg %s" %
484
                                    self._vg_name)
485
    pvs_info.sort()
486
    pvs_info.reverse()
487
    free_size, pv_name = pvs_info[0]
488
    if free_size < size:
489
      raise errors.BlockDeviceError("Not enough free space: required %s,"
490
                                    " available %s" % (size, free_size))
491

    
492
    result = utils.RunCmd(["lvcreate", "-L%dm" % size, "-s",
493
                           "-n%s" % snap_name, self.dev_path])
494
    if result.failed:
495
      raise errors.BlockDeviceError("command: %s error: %s" %
496
                                    (result.cmd, result.fail_reason))
497

    
498
    return snap_name
499

    
500
  def SetInfo(self, text):
501
    """Update metadata with info text.
502

503
    """
504
    BlockDev.SetInfo(self, text)
505

    
506
    # Replace invalid characters
507
    text = re.sub('^[^A-Za-z0-9_+.]', '_', text)
508
    text = re.sub('[^-A-Za-z0-9_+.]', '_', text)
509

    
510
    # Only up to 128 characters are allowed
511
    text = text[:128]
512

    
513
    result = utils.RunCmd(["lvchange", "--addtag", text,
514
                           self.dev_path])
515
    if result.failed:
516
      raise errors.BlockDeviceError("Command: %s error: %s" %
517
                                    (result.cmd, result.fail_reason))
518

    
519

    
520
class MDRaid1(BlockDev):
521
  """raid1 device implemented via md.
522

523
  """
524
  def __init__(self, unique_id, children):
525
    super(MDRaid1, self).__init__(unique_id, children)
526
    self.major = 9
527
    self.Attach()
528

    
529
  def Attach(self):
530
    """Find an array which matches our config and attach to it.
531

532
    This tries to find a MD array which has the same UUID as our own.
533

534
    """
535
    minor = self._FindMDByUUID(self.unique_id)
536
    if minor is not None:
537
      self._SetFromMinor(minor)
538
    else:
539
      self.minor = None
540
      self.dev_path = None
541

    
542
    return (minor is not None)
543

    
544
  @staticmethod
545
  def _GetUsedDevs():
546
    """Compute the list of in-use MD devices.
547

548
    It doesn't matter if the used device have other raid level, just
549
    that they are in use.
550

551
    """
552
    mdstat = open("/proc/mdstat", "r")
553
    data = mdstat.readlines()
554
    mdstat.close()
555

    
556
    used_md = {}
557
    valid_line = re.compile("^md([0-9]+) : .*$")
558
    for line in data:
559
      match = valid_line.match(line)
560
      if match:
561
        md_no = int(match.group(1))
562
        used_md[md_no] = line
563

    
564
    return used_md
565

    
566
  @staticmethod
567
  def _GetDevInfo(minor):
568
    """Get info about a MD device.
569

570
    Currently only uuid is returned.
571

572
    """
573
    result = utils.RunCmd(["mdadm", "-D", "/dev/md%d" % minor])
574
    if result.failed:
575
      logger.Error("Can't display md: %s" % result.fail_reason)
576
      return None
577
    retval = {}
578
    for line in result.stdout.splitlines():
579
      line = line.strip()
580
      kv = line.split(" : ", 1)
581
      if kv:
582
        if kv[0] == "UUID":
583
          retval["uuid"] = kv[1].split()[0]
584
        elif kv[0] == "State":
585
          retval["state"] = kv[1].split(", ")
586
    return retval
587

    
588
  @staticmethod
589
  def _FindUnusedMinor():
590
    """Compute an unused MD minor.
591

592
    This code assumes that there are 256 minors only.
593

594
    """
595
    used_md = MDRaid1._GetUsedDevs()
596
    i = 0
597
    while i < 256:
598
      if i not in used_md:
599
        break
600
      i += 1
601
    if i == 256:
602
      logger.Error("Critical: Out of md minor numbers.")
603
      raise errors.BlockDeviceError("Can't find a free MD minor")
604
    return i
605

    
606
  @classmethod
607
  def _FindMDByUUID(cls, uuid):
608
    """Find the minor of an MD array with a given UUID.
609

610
    """
611
    md_list = cls._GetUsedDevs()
612
    for minor in md_list:
613
      info = cls._GetDevInfo(minor)
614
      if info and info["uuid"] == uuid:
615
        return minor
616
    return None
617

    
618
  @staticmethod
619
  def _ZeroSuperblock(dev_path):
620
    """Zero the possible locations for an MD superblock.
621

622
    The zero-ing can't be done via ``mdadm --zero-superblock`` as that
623
    fails in versions 2.x with the same error code as non-writable
624
    device.
625

626
    The superblocks are located at (negative values are relative to
627
    the end of the block device):
628
      - -128k to end for version 0.90 superblock
629
      - -8k to -12k for version 1.0 superblock (included in the above)
630
      - 0k to 4k for version 1.1 superblock
631
      - 4k to 8k for version 1.2 superblock
632

633
    To cover all situations, the zero-ing will be:
634
      - 0k to 128k
635
      - -128k to end
636

637
    As such, the minimum device size must be 128k, otherwise we'll get
638
    I/O errors.
639

640
    Note that this function depends on the fact that one can open,
641
    read and write block devices normally.
642

643
    """
644
    overwrite_size = 128 * 1024
645
    empty_buf = '\0' * overwrite_size
646
    fd = open(dev_path, "r+")
647
    try:
648
      fd.seek(0, 0)
649
      p1 = fd.tell()
650
      fd.write(empty_buf)
651
      p2 = fd.tell()
652
      logger.Debug("Zeroed %s from %d to %d" % (dev_path, p1, p2))
653
      fd.seek(-overwrite_size, 2)
654
      p1 = fd.tell()
655
      fd.write(empty_buf)
656
      p2 = fd.tell()
657
      logger.Debug("Zeroed %s from %d to %d" % (dev_path, p1, p2))
658
    finally:
659
      fd.close()
660

    
661
  @classmethod
662
  def Create(cls, unique_id, children, size):
663
    """Create a new MD raid1 array.
664

665
    """
666
    if not isinstance(children, (tuple, list)):
667
      raise ValueError("Invalid setup data for MDRaid1 dev: %s" %
668
                       str(children))
669
    for i in children:
670
      if not isinstance(i, BlockDev):
671
        raise ValueError("Invalid member in MDRaid1 dev: %s" % type(i))
672
    for i in children:
673
      try:
674
        cls._ZeroSuperblock(i.dev_path)
675
      except EnvironmentError, err:
676
        logger.Error("Can't zero superblock for %s: %s" %
677
                     (i.dev_path, str(err)))
678
        return None
679
    minor = cls._FindUnusedMinor()
680
    result = utils.RunCmd(["mdadm", "--create", "/dev/md%d" % minor,
681
                           "--auto=yes", "--force", "-l1",
682
                           "-n%d" % len(children)] +
683
                          [dev.dev_path for dev in children])
684

    
685
    if result.failed:
686
      logger.Error("Can't create md: %s: %s" % (result.fail_reason,
687
                                                result.output))
688
      return None
689
    info = cls._GetDevInfo(minor)
690
    if not info or not "uuid" in info:
691
      logger.Error("Wrong information returned from mdadm -D: %s" % str(info))
692
      return None
693
    return MDRaid1(info["uuid"], children)
694

    
695
  def Remove(self):
696
    """Stub remove function for MD RAID 1 arrays.
697

698
    We don't remove the superblock right now. Mark a to do.
699

700
    """
701
    #TODO: maybe zero superblock on child devices?
702
    return self.Shutdown()
703

    
704
  def Rename(self, new_id):
705
    """Rename a device.
706

707
    This is not supported for md raid1 devices.
708

709
    """
710
    raise errors.ProgrammerError("Can't rename a md raid1 device")
711

    
712
  def AddChildren(self, devices):
713
    """Add new member(s) to the md raid1.
714

715
    """
716
    if self.minor is None and not self.Attach():
717
      raise errors.BlockDeviceError("Can't attach to device")
718

    
719
    args = ["mdadm", "-a", self.dev_path]
720
    for dev in devices:
721
      if dev.dev_path is None:
722
        raise errors.BlockDeviceError("Child '%s' is not initialised" % dev)
723
      dev.Open()
724
      args.append(dev.dev_path)
725
    result = utils.RunCmd(args)
726
    if result.failed:
727
      raise errors.BlockDeviceError("Failed to add new device to array: %s" %
728
                                    result.output)
729
    new_len = len(self._children) + len(devices)
730
    result = utils.RunCmd(["mdadm", "--grow", self.dev_path, "-n", new_len])
731
    if result.failed:
732
      raise errors.BlockDeviceError("Can't grow md array: %s" %
733
                                    result.output)
734
    self._children.extend(devices)
735

    
736
  def RemoveChildren(self, devices):
737
    """Remove member(s) from the md raid1.
738

739
    """
740
    if self.minor is None and not self.Attach():
741
      raise errors.BlockDeviceError("Can't attach to device")
742
    new_len = len(self._children) - len(devices)
743
    if new_len < 1:
744
      raise errors.BlockDeviceError("Can't reduce to less than one child")
745
    args = ["mdadm", "-f", self.dev_path]
746
    orig_devs = []
747
    for dev in devices:
748
      args.append(dev)
749
      for c in self._children:
750
        if c.dev_path == dev:
751
          orig_devs.append(c)
752
          break
753
      else:
754
        raise errors.BlockDeviceError("Can't find device '%s' for removal" %
755
                                      dev)
756
    result = utils.RunCmd(args)
757
    if result.failed:
758
      raise errors.BlockDeviceError("Failed to mark device(s) as failed: %s" %
759
                                    result.output)
760

    
761
    # it seems here we need a short delay for MD to update its
762
    # superblocks
763
    time.sleep(0.5)
764
    args[1] = "-r"
765
    result = utils.RunCmd(args)
766
    if result.failed:
767
      raise errors.BlockDeviceError("Failed to remove device(s) from array:"
768
                                    " %s" % result.output)
769
    result = utils.RunCmd(["mdadm", "--grow", "--force", self.dev_path,
770
                           "-n", new_len])
771
    if result.failed:
772
      raise errors.BlockDeviceError("Can't shrink md array: %s" %
773
                                    result.output)
774
    for dev in orig_devs:
775
      self._children.remove(dev)
776

    
777
  def _SetFromMinor(self, minor):
778
    """Set our parameters based on the given minor.
779

780
    This sets our minor variable and our dev_path.
781

782
    """
783
    self.minor = minor
784
    self.dev_path = "/dev/md%d" % minor
785

    
786
  def Assemble(self):
787
    """Assemble the MD device.
788

789
    At this point we should have:
790
      - list of children devices
791
      - uuid
792

793
    """
794
    result = super(MDRaid1, self).Assemble()
795
    if not result:
796
      return result
797
    md_list = self._GetUsedDevs()
798
    for minor in md_list:
799
      info = self._GetDevInfo(minor)
800
      if info and info["uuid"] == self.unique_id:
801
        self._SetFromMinor(minor)
802
        logger.Info("MD array %s already started" % str(self))
803
        return True
804
    free_minor = self._FindUnusedMinor()
805
    result = utils.RunCmd(["mdadm", "-A", "--auto=yes", "--uuid",
806
                           self.unique_id, "/dev/md%d" % free_minor] +
807
                          [bdev.dev_path for bdev in self._children])
808
    if result.failed:
809
      logger.Error("Can't assemble MD array: %s: %s" %
810
                   (result.fail_reason, result.output))
811
      self.minor = None
812
    else:
813
      self.minor = free_minor
814
    return not result.failed
815

    
816
  def Shutdown(self):
817
    """Tear down the MD array.
818

819
    This does a 'mdadm --stop' so after this command, the array is no
820
    longer available.
821

822
    """
823
    if self.minor is None and not self.Attach():
824
      logger.Info("MD object not attached to a device")
825
      return True
826

    
827
    result = utils.RunCmd(["mdadm", "--stop", "/dev/md%d" % self.minor])
828
    if result.failed:
829
      logger.Error("Can't stop MD array: %s" % result.fail_reason)
830
      return False
831
    self.minor = None
832
    self.dev_path = None
833
    return True
834

    
835
  def SetSyncSpeed(self, kbytes):
836
    """Set the maximum sync speed for the MD array.
837

838
    """
839
    result = super(MDRaid1, self).SetSyncSpeed(kbytes)
840
    if self.minor is None:
841
      logger.Error("MD array not attached to a device")
842
      return False
843
    f = open("/sys/block/md%d/md/sync_speed_max" % self.minor, "w")
844
    try:
845
      f.write("%d" % kbytes)
846
    finally:
847
      f.close()
848
    f = open("/sys/block/md%d/md/sync_speed_min" % self.minor, "w")
849
    try:
850
      f.write("%d" % (kbytes/2))
851
    finally:
852
      f.close()
853
    return result
854

    
855
  def GetSyncStatus(self):
856
    """Returns the sync status of the device.
857

858
    Returns:
859
     (sync_percent, estimated_time, is_degraded, ldisk)
860

861
    If sync_percent is None, it means all is ok
862
    If estimated_time is None, it means we can't esimate
863
    the time needed, otherwise it's the time left in seconds.
864

865
    The ldisk parameter is always true for MD devices.
866

867
    """
868
    if self.minor is None and not self.Attach():
869
      raise errors.BlockDeviceError("Can't attach to device in GetSyncStatus")
870
    dev_info = self._GetDevInfo(self.minor)
871
    is_clean = ("state" in dev_info and
872
                len(dev_info["state"]) == 1 and
873
                dev_info["state"][0] in ("clean", "active"))
874
    sys_path = "/sys/block/md%s/md/" % self.minor
875
    f = file(sys_path + "sync_action")
876
    sync_status = f.readline().strip()
877
    f.close()
878
    if sync_status == "idle":
879
      return None, None, not is_clean, False
880
    f = file(sys_path + "sync_completed")
881
    sync_completed = f.readline().strip().split(" / ")
882
    f.close()
883
    if len(sync_completed) != 2:
884
      return 0, None, not is_clean, False
885
    sync_done, sync_total = [float(i) for i in sync_completed]
886
    sync_percent = 100.0*sync_done/sync_total
887
    f = file(sys_path + "sync_speed")
888
    sync_speed_k = int(f.readline().strip())
889
    if sync_speed_k == 0:
890
      time_est = None
891
    else:
892
      time_est = (sync_total - sync_done) / 2 / sync_speed_k
893
    return sync_percent, time_est, not is_clean, False
894

    
895
  def Open(self, force=False):
896
    """Make the device ready for I/O.
897

898
    This is a no-op for the MDRaid1 device type, although we could use
899
    the 2.6.18's new array_state thing.
900

901
    """
902
    pass
903

    
904
  def Close(self):
905
    """Notifies that the device will no longer be used for I/O.
906

907
    This is a no-op for the MDRaid1 device type, but see comment for
908
    `Open()`.
909

910
    """
911
    pass
912

    
913

    
914
class BaseDRBD(BlockDev):
915
  """Base DRBD class.
916

917
  This class contains a few bits of common functionality between the
918
  0.7 and 8.x versions of DRBD.
919

920
  """
921
  _VERSION_RE = re.compile(r"^version: (\d+)\.(\d+)\.(\d+)"
922
                           r" \(api:(\d+)/proto:(\d+)(?:-(\d+))?\)")
923

    
924
  _DRBD_MAJOR = 147
925
  _ST_UNCONFIGURED = "Unconfigured"
926
  _ST_WFCONNECTION = "WFConnection"
927
  _ST_CONNECTED = "Connected"
928

    
929
  @staticmethod
930
  def _GetProcData():
931
    """Return data from /proc/drbd.
932

933
    """
934
    stat = open("/proc/drbd", "r")
935
    try:
936
      data = stat.read().splitlines()
937
    finally:
938
      stat.close()
939
    if not data:
940
      raise errors.BlockDeviceError("Can't read any data from /proc/drbd")
941
    return data
942

    
943
  @staticmethod
944
  def _MassageProcData(data):
945
    """Transform the output of _GetProdData into a nicer form.
946

947
    Returns:
948
      a dictionary of minor: joined lines from /proc/drbd for that minor
949

950
    """
951
    lmatch = re.compile("^ *([0-9]+):.*$")
952
    results = {}
953
    old_minor = old_line = None
954
    for line in data:
955
      lresult = lmatch.match(line)
956
      if lresult is not None:
957
        if old_minor is not None:
958
          results[old_minor] = old_line
959
        old_minor = int(lresult.group(1))
960
        old_line = line
961
      else:
962
        if old_minor is not None:
963
          old_line += " " + line.strip()
964
    # add last line
965
    if old_minor is not None:
966
      results[old_minor] = old_line
967
    return results
968

    
969
  @classmethod
970
  def _GetVersion(cls):
971
    """Return the DRBD version.
972

973
    This will return a dict with keys:
974
      k_major,
975
      k_minor,
976
      k_point,
977
      api,
978
      proto,
979
      proto2 (only on drbd > 8.2.X)
980

981
    """
982
    proc_data = cls._GetProcData()
983
    first_line = proc_data[0].strip()
984
    version = cls._VERSION_RE.match(first_line)
985
    if not version:
986
      raise errors.BlockDeviceError("Can't parse DRBD version from '%s'" %
987
                                    first_line)
988

    
989
    values = version.groups()
990
    retval = {'k_major': int(values[0]),
991
              'k_minor': int(values[1]),
992
              'k_point': int(values[2]),
993
              'api': int(values[3]),
994
              'proto': int(values[4]),
995
             }
996
    if values[5] is not None:
997
      retval['proto2'] = values[5]
998

    
999
    return retval
1000

    
1001
  @staticmethod
1002
  def _DevPath(minor):
1003
    """Return the path to a drbd device for a given minor.
1004

1005
    """
1006
    return "/dev/drbd%d" % minor
1007

    
1008
  @classmethod
1009
  def _GetUsedDevs(cls):
1010
    """Compute the list of used DRBD devices.
1011

1012
    """
1013
    data = cls._GetProcData()
1014

    
1015
    used_devs = {}
1016
    valid_line = re.compile("^ *([0-9]+): cs:([^ ]+).*$")
1017
    for line in data:
1018
      match = valid_line.match(line)
1019
      if not match:
1020
        continue
1021
      minor = int(match.group(1))
1022
      state = match.group(2)
1023
      if state == cls._ST_UNCONFIGURED:
1024
        continue
1025
      used_devs[minor] = state, line
1026

    
1027
    return used_devs
1028

    
1029
  def _SetFromMinor(self, minor):
1030
    """Set our parameters based on the given minor.
1031

1032
    This sets our minor variable and our dev_path.
1033

1034
    """
1035
    if minor is None:
1036
      self.minor = self.dev_path = None
1037
    else:
1038
      self.minor = minor
1039
      self.dev_path = self._DevPath(minor)
1040

    
1041
  @staticmethod
1042
  def _CheckMetaSize(meta_device):
1043
    """Check if the given meta device looks like a valid one.
1044

1045
    This currently only check the size, which must be around
1046
    128MiB.
1047

1048
    """
1049
    result = utils.RunCmd(["blockdev", "--getsize", meta_device])
1050
    if result.failed:
1051
      logger.Error("Failed to get device size: %s" % result.fail_reason)
1052
      return False
1053
    try:
1054
      sectors = int(result.stdout)
1055
    except ValueError:
1056
      logger.Error("Invalid output from blockdev: '%s'" % result.stdout)
1057
      return False
1058
    bytes = sectors * 512
1059
    if bytes < 128 * 1024 * 1024: # less than 128MiB
1060
      logger.Error("Meta device too small (%.2fMib)" % (bytes / 1024 / 1024))
1061
      return False
1062
    if bytes > (128 + 32) * 1024 * 1024: # account for an extra (big) PE on LVM
1063
      logger.Error("Meta device too big (%.2fMiB)" % (bytes / 1024 / 1024))
1064
      return False
1065
    return True
1066

    
1067
  def Rename(self, new_id):
1068
    """Rename a device.
1069

1070
    This is not supported for drbd devices.
1071

1072
    """
1073
    raise errors.ProgrammerError("Can't rename a drbd device")
1074

    
1075

    
1076
class DRBDev(BaseDRBD):
1077
  """DRBD block device.
1078

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

1083
  The unique_id for the drbd device is the (local_ip, local_port,
1084
  remote_ip, remote_port) tuple, and it must have two children: the
1085
  data device and the meta_device. The meta device is checked for
1086
  valid size and is zeroed on create.
1087

1088
  """
1089
  def __init__(self, unique_id, children):
1090
    super(DRBDev, self).__init__(unique_id, children)
1091
    self.major = self._DRBD_MAJOR
1092
    version = self._GetVersion()
1093
    if version['k_major'] != 0 and version['k_minor'] != 7:
1094
      raise errors.BlockDeviceError("Mismatch in DRBD kernel version and"
1095
                                    " requested ganeti usage: kernel is"
1096
                                    " %s.%s, ganeti wants 0.7" %
1097
                                    (version['k_major'], version['k_minor']))
1098
    if len(children) != 2:
1099
      raise ValueError("Invalid configuration data %s" % str(children))
1100
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 4:
1101
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1102
    self._lhost, self._lport, self._rhost, self._rport = unique_id
1103
    self.Attach()
1104

    
1105
  @classmethod
1106
  def _FindUnusedMinor(cls):
1107
    """Find an unused DRBD device.
1108

1109
    """
1110
    data = cls._GetProcData()
1111

    
1112
    valid_line = re.compile("^ *([0-9]+): cs:Unconfigured$")
1113
    for line in data:
1114
      match = valid_line.match(line)
1115
      if match:
1116
        return int(match.group(1))
1117
    logger.Error("Error: no free drbd minors!")
1118
    raise errors.BlockDeviceError("Can't find a free DRBD minor")
1119

    
1120
  @classmethod
1121
  def _GetDevInfo(cls, minor):
1122
    """Get details about a given DRBD minor.
1123

1124
    This return, if available, the local backing device in (major,
1125
    minor) formant and the local and remote (ip, port) information.
1126

1127
    """
1128
    data = {}
1129
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "show"])
1130
    if result.failed:
1131
      logger.Error("Can't display the drbd config: %s" % result.fail_reason)
1132
      return data
1133
    out = result.stdout
1134
    if out == "Not configured\n":
1135
      return data
1136
    for line in out.splitlines():
1137
      if "local_dev" not in data:
1138
        match = re.match("^Lower device: ([0-9]+):([0-9]+) .*$", line)
1139
        if match:
1140
          data["local_dev"] = (int(match.group(1)), int(match.group(2)))
1141
          continue
1142
      if "meta_dev" not in data:
1143
        match = re.match("^Meta device: (([0-9]+):([0-9]+)|internal).*$", line)
1144
        if match:
1145
          if match.group(2) is not None and match.group(3) is not None:
1146
            # matched on the major/minor
1147
            data["meta_dev"] = (int(match.group(2)), int(match.group(3)))
1148
          else:
1149
            # matched on the "internal" string
1150
            data["meta_dev"] = match.group(1)
1151
            # in this case, no meta_index is in the output
1152
            data["meta_index"] = -1
1153
          continue
1154
      if "meta_index" not in data:
1155
        match = re.match("^Meta index: ([0-9]+).*$", line)
1156
        if match:
1157
          data["meta_index"] = int(match.group(1))
1158
          continue
1159
      if "local_addr" not in data:
1160
        match = re.match("^Local address: ([0-9.]+):([0-9]+)$", line)
1161
        if match:
1162
          data["local_addr"] = (match.group(1), int(match.group(2)))
1163
          continue
1164
      if "remote_addr" not in data:
1165
        match = re.match("^Remote address: ([0-9.]+):([0-9]+)$", line)
1166
        if match:
1167
          data["remote_addr"] = (match.group(1), int(match.group(2)))
1168
          continue
1169
    return data
1170

    
1171
  def _MatchesLocal(self, info):
1172
    """Test if our local config matches with an existing device.
1173

1174
    The parameter should be as returned from `_GetDevInfo()`. This
1175
    method tests if our local backing device is the same as the one in
1176
    the info parameter, in effect testing if we look like the given
1177
    device.
1178

1179
    """
1180
    if not ("local_dev" in info and "meta_dev" in info and
1181
            "meta_index" in info):
1182
      return False
1183

    
1184
    backend = self._children[0]
1185
    if backend is not None:
1186
      retval = (info["local_dev"] == (backend.major, backend.minor))
1187
    else:
1188
      retval = (info["local_dev"] == (0, 0))
1189
    meta = self._children[1]
1190
    if meta is not None:
1191
      retval = retval and (info["meta_dev"] == (meta.major, meta.minor))
1192
      retval = retval and (info["meta_index"] == 0)
1193
    else:
1194
      retval = retval and (info["meta_dev"] == "internal" and
1195
                           info["meta_index"] == -1)
1196
    return retval
1197

    
1198
  def _MatchesNet(self, info):
1199
    """Test if our network config matches with an existing device.
1200

1201
    The parameter should be as returned from `_GetDevInfo()`. This
1202
    method tests if our network configuration is the same as the one
1203
    in the info parameter, in effect testing if we look like the given
1204
    device.
1205

1206
    """
1207
    if (((self._lhost is None and not ("local_addr" in info)) and
1208
         (self._rhost is None and not ("remote_addr" in info)))):
1209
      return True
1210

    
1211
    if self._lhost is None:
1212
      return False
1213

    
1214
    if not ("local_addr" in info and
1215
            "remote_addr" in info):
1216
      return False
1217

    
1218
    retval = (info["local_addr"] == (self._lhost, self._lport))
1219
    retval = (retval and
1220
              info["remote_addr"] == (self._rhost, self._rport))
1221
    return retval
1222

    
1223
  @classmethod
1224
  def _AssembleLocal(cls, minor, backend, meta):
1225
    """Configure the local part of a DRBD device.
1226

1227
    This is the first thing that must be done on an unconfigured DRBD
1228
    device. And it must be done only once.
1229

1230
    """
1231
    if not cls._CheckMetaSize(meta):
1232
      return False
1233
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disk",
1234
                           backend, meta, "0", "-e", "detach"])
1235
    if result.failed:
1236
      logger.Error("Can't attach local disk: %s" % result.output)
1237
    return not result.failed
1238

    
1239
  @classmethod
1240
  def _ShutdownLocal(cls, minor):
1241
    """Detach from the local device.
1242

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

1246
    """
1247
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "detach"])
1248
    if result.failed:
1249
      logger.Error("Can't detach local device: %s" % result.output)
1250
    return not result.failed
1251

    
1252
  @staticmethod
1253
  def _ShutdownAll(minor):
1254
    """Deactivate the device.
1255

1256
    This will, of course, fail if the device is in use.
1257

1258
    """
1259
    result = utils.RunCmd(["drbdsetup", DRBDev._DevPath(minor), "down"])
1260
    if result.failed:
1261
      logger.Error("Can't shutdown drbd device: %s" % result.output)
1262
    return not result.failed
1263

    
1264
  @classmethod
1265
  def _AssembleNet(cls, minor, net_info, protocol):
1266
    """Configure the network part of the device.
1267

1268
    This operation can be, in theory, done multiple times, but there
1269
    have been cases (in lab testing) in which the network part of the
1270
    device had become stuck and couldn't be shut down because activity
1271
    from the new peer (also stuck) triggered a timer re-init and
1272
    needed remote peer interface shutdown in order to clear. So please
1273
    don't change online the net config.
1274

1275
    """
1276
    lhost, lport, rhost, rport = net_info
1277
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "net",
1278
                           "%s:%s" % (lhost, lport), "%s:%s" % (rhost, rport),
1279
                           protocol])
1280
    if result.failed:
1281
      logger.Error("Can't setup network for dbrd device: %s" %
1282
                   result.fail_reason)
1283
      return False
1284

    
1285
    timeout = time.time() + 10
1286
    ok = False
1287
    while time.time() < timeout:
1288
      info = cls._GetDevInfo(minor)
1289
      if not "local_addr" in info or not "remote_addr" in info:
1290
        time.sleep(1)
1291
        continue
1292
      if (info["local_addr"] != (lhost, lport) or
1293
          info["remote_addr"] != (rhost, rport)):
1294
        time.sleep(1)
1295
        continue
1296
      ok = True
1297
      break
1298
    if not ok:
1299
      logger.Error("Timeout while configuring network")
1300
      return False
1301
    return True
1302

    
1303
  @classmethod
1304
  def _ShutdownNet(cls, minor):
1305
    """Disconnect from the remote peer.
1306

1307
    This fails if we don't have a local device.
1308

1309
    """
1310
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disconnect"])
1311
    if result.failed:
1312
      logger.Error("Can't shutdown network: %s" % result.output)
1313
    return not result.failed
1314

    
1315
  def Assemble(self):
1316
    """Assemble the drbd.
1317

1318
    Method:
1319
      - if we have a local backing device, we bind to it by:
1320
        - checking the list of used drbd devices
1321
        - check if the local minor use of any of them is our own device
1322
        - if yes, abort?
1323
        - if not, bind
1324
      - if we have a local/remote net info:
1325
        - redo the local backing device step for the remote device
1326
        - check if any drbd device is using the local port,
1327
          if yes abort
1328
        - check if any remote drbd device is using the remote
1329
          port, if yes abort (for now)
1330
        - bind our net port
1331
        - bind the remote net port
1332

1333
    """
1334
    self.Attach()
1335
    if self.minor is not None:
1336
      logger.Info("Already assembled")
1337
      return True
1338

    
1339
    result = super(DRBDev, self).Assemble()
1340
    if not result:
1341
      return result
1342

    
1343
    minor = self._FindUnusedMinor()
1344
    need_localdev_teardown = False
1345
    if self._children[0]:
1346
      result = self._AssembleLocal(minor, self._children[0].dev_path,
1347
                                   self._children[1].dev_path)
1348
      if not result:
1349
        return False
1350
      need_localdev_teardown = True
1351
    if self._lhost and self._lport and self._rhost and self._rport:
1352
      result = self._AssembleNet(minor,
1353
                                 (self._lhost, self._lport,
1354
                                  self._rhost, self._rport),
1355
                                 "C")
1356
      if not result:
1357
        if need_localdev_teardown:
1358
          # we will ignore failures from this
1359
          logger.Error("net setup failed, tearing down local device")
1360
          self._ShutdownAll(minor)
1361
        return False
1362
    self._SetFromMinor(minor)
1363
    return True
1364

    
1365
  def Shutdown(self):
1366
    """Shutdown the DRBD device.
1367

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

    
1378
  def Attach(self):
1379
    """Find a DRBD device which matches our config and attach to it.
1380

1381
    In case of partially attached (local device matches but no network
1382
    setup), we perform the network attach. If successful, we re-test
1383
    the attach if can return success.
1384

1385
    """
1386
    for minor in self._GetUsedDevs():
1387
      info = self._GetDevInfo(minor)
1388
      match_l = self._MatchesLocal(info)
1389
      match_r = self._MatchesNet(info)
1390
      if match_l and match_r:
1391
        break
1392
      if match_l and not match_r and "local_addr" not in info:
1393
        res_r = self._AssembleNet(minor,
1394
                                  (self._lhost, self._lport,
1395
                                   self._rhost, self._rport),
1396
                                  "C")
1397
        if res_r and self._MatchesNet(self._GetDevInfo(minor)):
1398
          break
1399
    else:
1400
      minor = None
1401

    
1402
    self._SetFromMinor(minor)
1403
    return minor is not None
1404

    
1405
  def Open(self, force=False):
1406
    """Make the local state primary.
1407

1408
    If the 'force' parameter is given, the '--do-what-I-say' parameter
1409
    is given. Since this is a pottentialy dangerous operation, the
1410
    force flag should be only given after creation, when it actually
1411
    has to be given.
1412

1413
    """
1414
    if self.minor is None and not self.Attach():
1415
      logger.Error("DRBD cannot attach to a device during open")
1416
      return False
1417
    cmd = ["drbdsetup", self.dev_path, "primary"]
1418
    if force:
1419
      cmd.append("--do-what-I-say")
1420
    result = utils.RunCmd(cmd)
1421
    if result.failed:
1422
      msg = ("Can't make drbd device primary: %s" % result.output)
1423
      logger.Error(msg)
1424
      raise errors.BlockDeviceError(msg)
1425

    
1426
  def Close(self):
1427
    """Make the local state secondary.
1428

1429
    This will, of course, fail if the device is in use.
1430

1431
    """
1432
    if self.minor is None and not self.Attach():
1433
      logger.Info("Instance not attached to a device")
1434
      raise errors.BlockDeviceError("Can't find device")
1435
    result = utils.RunCmd(["drbdsetup", self.dev_path, "secondary"])
1436
    if result.failed:
1437
      msg = ("Can't switch drbd device to"
1438
             " secondary: %s" % result.output)
1439
      logger.Error(msg)
1440
      raise errors.BlockDeviceError(msg)
1441

    
1442
  def SetSyncSpeed(self, kbytes):
1443
    """Set the speed of the DRBD syncer.
1444

1445
    """
1446
    children_result = super(DRBDev, self).SetSyncSpeed(kbytes)
1447
    if self.minor is None:
1448
      logger.Info("Instance not attached to a device")
1449
      return False
1450
    result = utils.RunCmd(["drbdsetup", self.dev_path, "syncer", "-r", "%d" %
1451
                           kbytes])
1452
    if result.failed:
1453
      logger.Error("Can't change syncer rate: %s " % result.fail_reason)
1454
    return not result.failed and children_result
1455

    
1456
  def GetSyncStatus(self):
1457
    """Returns the sync status of the device.
1458

1459
    Returns:
1460
     (sync_percent, estimated_time, is_degraded, ldisk)
1461

1462
    If sync_percent is None, it means all is ok
1463
    If estimated_time is None, it means we can't esimate
1464
    the time needed, otherwise it's the time left in seconds.
1465

1466
    The ldisk parameter will be returned as True, since the DRBD7
1467
    devices have not been converted.
1468

1469
    """
1470
    if self.minor is None and not self.Attach():
1471
      raise errors.BlockDeviceError("Can't attach to device in GetSyncStatus")
1472
    proc_info = self._MassageProcData(self._GetProcData())
1473
    if self.minor not in proc_info:
1474
      raise errors.BlockDeviceError("Can't find myself in /proc (minor %d)" %
1475
                                    self.minor)
1476
    line = proc_info[self.minor]
1477
    match = re.match("^.*sync'ed: *([0-9.]+)%.*"
1478
                     " finish: ([0-9]+):([0-9]+):([0-9]+) .*$", line)
1479
    if match:
1480
      sync_percent = float(match.group(1))
1481
      hours = int(match.group(2))
1482
      minutes = int(match.group(3))
1483
      seconds = int(match.group(4))
1484
      est_time = hours * 3600 + minutes * 60 + seconds
1485
    else:
1486
      sync_percent = None
1487
      est_time = None
1488
    match = re.match("^ *[0-9]+: cs:([^ ]+).*$", line)
1489
    if not match:
1490
      raise errors.BlockDeviceError("Can't find my data in /proc (minor %d)" %
1491
                                    self.minor)
1492
    client_state = match.group(1)
1493
    is_degraded = client_state != "Connected"
1494
    return sync_percent, est_time, is_degraded, False
1495

    
1496
  @staticmethod
1497
  def _ZeroDevice(device):
1498
    """Zero a device.
1499

1500
    This writes until we get ENOSPC.
1501

1502
    """
1503
    f = open(device, "w")
1504
    buf = "\0" * 1048576
1505
    try:
1506
      while True:
1507
        f.write(buf)
1508
    except IOError, err:
1509
      if err.errno != errno.ENOSPC:
1510
        raise
1511

    
1512
  @classmethod
1513
  def Create(cls, unique_id, children, size):
1514
    """Create a new DRBD device.
1515

1516
    Since DRBD devices are not created per se, just assembled, this
1517
    function just zeroes the meta device.
1518

1519
    """
1520
    if len(children) != 2:
1521
      raise errors.ProgrammerError("Invalid setup for the drbd device")
1522
    meta = children[1]
1523
    meta.Assemble()
1524
    if not meta.Attach():
1525
      raise errors.BlockDeviceError("Can't attach to meta device")
1526
    if not cls._CheckMetaSize(meta.dev_path):
1527
      raise errors.BlockDeviceError("Invalid meta device")
1528
    logger.Info("Started zeroing device %s" % meta.dev_path)
1529
    cls._ZeroDevice(meta.dev_path)
1530
    logger.Info("Done zeroing device %s" % meta.dev_path)
1531
    return cls(unique_id, children)
1532

    
1533
  def Remove(self):
1534
    """Stub remove for DRBD devices.
1535

1536
    """
1537
    return self.Shutdown()
1538

    
1539

    
1540
class DRBD8(BaseDRBD):
1541
  """DRBD v8.x block device.
1542

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

1547
  The unique_id for the drbd device is the (local_ip, local_port,
1548
  remote_ip, remote_port) tuple, and it must have two children: the
1549
  data device and the meta_device. The meta device is checked for
1550
  valid size and is zeroed on create.
1551

1552
  """
1553
  _MAX_MINORS = 255
1554
  _PARSE_SHOW = None
1555

    
1556
  def __init__(self, unique_id, children):
1557
    if children and children.count(None) > 0:
1558
      children = []
1559
    super(DRBD8, self).__init__(unique_id, children)
1560
    self.major = self._DRBD_MAJOR
1561
    version = self._GetVersion()
1562
    if version['k_major'] != 8 :
1563
      raise errors.BlockDeviceError("Mismatch in DRBD kernel version and"
1564
                                    " requested ganeti usage: kernel is"
1565
                                    " %s.%s, ganeti wants 8.x" %
1566
                                    (version['k_major'], version['k_minor']))
1567

    
1568
    if len(children) not in (0, 2):
1569
      raise ValueError("Invalid configuration data %s" % str(children))
1570
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 4:
1571
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1572
    self._lhost, self._lport, self._rhost, self._rport = unique_id
1573
    self.Attach()
1574

    
1575
  @classmethod
1576
  def _InitMeta(cls, minor, dev_path):
1577
    """Initialize a meta device.
1578

1579
    This will not work if the given minor is in use.
1580

1581
    """
1582
    result = utils.RunCmd(["drbdmeta", "--force", cls._DevPath(minor),
1583
                           "v08", dev_path, "0", "create-md"])
1584
    if result.failed:
1585
      raise errors.BlockDeviceError("Can't initialize meta device: %s" %
1586
                                    result.output)
1587

    
1588
  @classmethod
1589
  def _FindUnusedMinor(cls):
1590
    """Find an unused DRBD device.
1591

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

1595
    """
1596
    data = cls._GetProcData()
1597

    
1598
    unused_line = re.compile("^ *([0-9]+): cs:Unconfigured$")
1599
    used_line = re.compile("^ *([0-9]+): cs:")
1600
    highest = None
1601
    for line in data:
1602
      match = unused_line.match(line)
1603
      if match:
1604
        return int(match.group(1))
1605
      match = used_line.match(line)
1606
      if match:
1607
        minor = int(match.group(1))
1608
        highest = max(highest, minor)
1609
    if highest is None: # there are no minors in use at all
1610
      return 0
1611
    if highest >= cls._MAX_MINORS:
1612
      logger.Error("Error: no free drbd minors!")
1613
      raise errors.BlockDeviceError("Can't find a free DRBD minor")
1614
    return highest + 1
1615

    
1616
  @classmethod
1617
  def _IsValidMeta(cls, meta_device):
1618
    """Check if the given meta device looks like a valid one.
1619

1620
    """
1621
    minor = cls._FindUnusedMinor()
1622
    minor_path = cls._DevPath(minor)
1623
    result = utils.RunCmd(["drbdmeta", minor_path,
1624
                           "v08", meta_device, "0",
1625
                           "dstate"])
1626
    if result.failed:
1627
      logger.Error("Invalid meta device %s: %s" % (meta_device, result.output))
1628
      return False
1629
    return True
1630

    
1631
  @classmethod
1632
  def _GetShowParser(cls):
1633
    """Return a parser for `drbd show` output.
1634

1635
    This will either create or return an already-create parser for the
1636
    output of the command `drbd show`.
1637

1638
    """
1639
    if cls._PARSE_SHOW is not None:
1640
      return cls._PARSE_SHOW
1641

    
1642
    # pyparsing setup
1643
    lbrace = pyp.Literal("{").suppress()
1644
    rbrace = pyp.Literal("}").suppress()
1645
    semi = pyp.Literal(";").suppress()
1646
    # this also converts the value to an int
1647
    number = pyp.Word(pyp.nums).setParseAction(lambda s, l, t: int(t[0]))
1648

    
1649
    comment = pyp.Literal ("#") + pyp.Optional(pyp.restOfLine)
1650
    defa = pyp.Literal("_is_default").suppress()
1651
    dbl_quote = pyp.Literal('"').suppress()
1652

    
1653
    keyword = pyp.Word(pyp.alphanums + '-')
1654

    
1655
    # value types
1656
    value = pyp.Word(pyp.alphanums + '_-/.:')
1657
    quoted = dbl_quote + pyp.CharsNotIn('"') + dbl_quote
1658
    addr_port = (pyp.Word(pyp.nums + '.') + pyp.Literal(':').suppress() +
1659
                 number)
1660
    # meta device, extended syntax
1661
    meta_value = ((value ^ quoted) + pyp.Literal('[').suppress() +
1662
                  number + pyp.Word(']').suppress())
1663

    
1664
    # a statement
1665
    stmt = (~rbrace + keyword + ~lbrace +
1666
            (addr_port ^ value ^ quoted ^ meta_value) +
1667
            pyp.Optional(defa) + semi +
1668
            pyp.Optional(pyp.restOfLine).suppress())
1669

    
1670
    # an entire section
1671
    section_name = pyp.Word(pyp.alphas + '_')
1672
    section = section_name + lbrace + pyp.ZeroOrMore(pyp.Group(stmt)) + rbrace
1673

    
1674
    bnf = pyp.ZeroOrMore(pyp.Group(section ^ stmt))
1675
    bnf.ignore(comment)
1676

    
1677
    cls._PARSE_SHOW = bnf
1678

    
1679
    return bnf
1680

    
1681
  @classmethod
1682
  def _GetShowData(cls, minor):
1683
    """Return the `drbdsetup show` data for a minor.
1684

1685
    """
1686
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "show"])
1687
    if result.failed:
1688
      logger.Error("Can't display the drbd config: %s" % result.fail_reason)
1689
      return None
1690
    return result.stdout
1691

    
1692
  @classmethod
1693
  def _GetDevInfo(cls, out):
1694
    """Parse details about a given DRBD minor.
1695

1696
    This return, if available, the local backing device (as a path)
1697
    and the local and remote (ip, port) information from a string
1698
    containing the output of the `drbdsetup show` command as returned
1699
    by _GetShowData.
1700

1701
    """
1702
    data = {}
1703
    if not out:
1704
      return data
1705

    
1706
    bnf = cls._GetShowParser()
1707
    # run pyparse
1708

    
1709
    try:
1710
      results = bnf.parseString(out)
1711
    except pyp.ParseException, err:
1712
      raise errors.BlockDeviceError("Can't parse drbdsetup show output: %s" %
1713
                                    str(err))
1714

    
1715
    # and massage the results into our desired format
1716
    for section in results:
1717
      sname = section[0]
1718
      if sname == "_this_host":
1719
        for lst in section[1:]:
1720
          if lst[0] == "disk":
1721
            data["local_dev"] = lst[1]
1722
          elif lst[0] == "meta-disk":
1723
            data["meta_dev"] = lst[1]
1724
            data["meta_index"] = lst[2]
1725
          elif lst[0] == "address":
1726
            data["local_addr"] = tuple(lst[1:])
1727
      elif sname == "_remote_host":
1728
        for lst in section[1:]:
1729
          if lst[0] == "address":
1730
            data["remote_addr"] = tuple(lst[1:])
1731
    return data
1732

    
1733
  def _MatchesLocal(self, info):
1734
    """Test if our local config matches with an existing device.
1735

1736
    The parameter should be as returned from `_GetDevInfo()`. This
1737
    method tests if our local backing device is the same as the one in
1738
    the info parameter, in effect testing if we look like the given
1739
    device.
1740

1741
    """
1742
    if self._children:
1743
      backend, meta = self._children
1744
    else:
1745
      backend = meta = None
1746

    
1747
    if backend is not None:
1748
      retval = ("local_dev" in info and info["local_dev"] == backend.dev_path)
1749
    else:
1750
      retval = ("local_dev" not in info)
1751

    
1752
    if meta is not None:
1753
      retval = retval and ("meta_dev" in info and
1754
                           info["meta_dev"] == meta.dev_path)
1755
      retval = retval and ("meta_index" in info and
1756
                           info["meta_index"] == 0)
1757
    else:
1758
      retval = retval and ("meta_dev" not in info and
1759
                           "meta_index" not in info)
1760
    return retval
1761

    
1762
  def _MatchesNet(self, info):
1763
    """Test if our network config matches with an existing device.
1764

1765
    The parameter should be as returned from `_GetDevInfo()`. This
1766
    method tests if our network configuration is the same as the one
1767
    in the info parameter, in effect testing if we look like the given
1768
    device.
1769

1770
    """
1771
    if (((self._lhost is None and not ("local_addr" in info)) and
1772
         (self._rhost is None and not ("remote_addr" in info)))):
1773
      return True
1774

    
1775
    if self._lhost is None:
1776
      return False
1777

    
1778
    if not ("local_addr" in info and
1779
            "remote_addr" in info):
1780
      return False
1781

    
1782
    retval = (info["local_addr"] == (self._lhost, self._lport))
1783
    retval = (retval and
1784
              info["remote_addr"] == (self._rhost, self._rport))
1785
    return retval
1786

    
1787
  @classmethod
1788
  def _AssembleLocal(cls, minor, backend, meta):
1789
    """Configure the local part of a DRBD device.
1790

1791
    This is the first thing that must be done on an unconfigured DRBD
1792
    device. And it must be done only once.
1793

1794
    """
1795
    if not cls._IsValidMeta(meta):
1796
      return False
1797
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disk",
1798
                           backend, meta, "0", "-e", "detach",
1799
                           "--create-device"])
1800
    if result.failed:
1801
      logger.Error("Can't attach local disk: %s" % result.output)
1802
    return not result.failed
1803

    
1804
  @classmethod
1805
  def _AssembleNet(cls, minor, net_info, protocol,
1806
                   dual_pri=False, hmac=None, secret=None):
1807
    """Configure the network part of the device.
1808

1809
    """
1810
    lhost, lport, rhost, rport = net_info
1811
    if None in net_info:
1812
      # we don't want network connection and actually want to make
1813
      # sure its shutdown
1814
      return cls._ShutdownNet(minor)
1815

    
1816
    args = ["drbdsetup", cls._DevPath(minor), "net",
1817
            "%s:%s" % (lhost, lport), "%s:%s" % (rhost, rport), protocol,
1818
            "-A", "discard-zero-changes",
1819
            "-B", "consensus",
1820
            ]
1821
    if dual_pri:
1822
      args.append("-m")
1823
    if hmac and secret:
1824
      args.extend(["-a", hmac, "-x", secret])
1825
    result = utils.RunCmd(args)
1826
    if result.failed:
1827
      logger.Error("Can't setup network for dbrd device: %s" %
1828
                   result.fail_reason)
1829
      return False
1830

    
1831
    timeout = time.time() + 10
1832
    ok = False
1833
    while time.time() < timeout:
1834
      info = cls._GetDevInfo(cls._GetShowData(minor))
1835
      if not "local_addr" in info or not "remote_addr" in info:
1836
        time.sleep(1)
1837
        continue
1838
      if (info["local_addr"] != (lhost, lport) or
1839
          info["remote_addr"] != (rhost, rport)):
1840
        time.sleep(1)
1841
        continue
1842
      ok = True
1843
      break
1844
    if not ok:
1845
      logger.Error("Timeout while configuring network")
1846
      return False
1847
    return True
1848

    
1849
  def AddChildren(self, devices):
1850
    """Add a disk to the DRBD device.
1851

1852
    """
1853
    if self.minor is None:
1854
      raise errors.BlockDeviceError("Can't attach to dbrd8 during AddChildren")
1855
    if len(devices) != 2:
1856
      raise errors.BlockDeviceError("Need two devices for AddChildren")
1857
    info = self._GetDevInfo(self._GetShowData(self.minor))
1858
    if "local_dev" in info:
1859
      raise errors.BlockDeviceError("DRBD8 already attached to a local disk")
1860
    backend, meta = devices
1861
    if backend.dev_path is None or meta.dev_path is None:
1862
      raise errors.BlockDeviceError("Children not ready during AddChildren")
1863
    backend.Open()
1864
    meta.Open()
1865
    if not self._CheckMetaSize(meta.dev_path):
1866
      raise errors.BlockDeviceError("Invalid meta device size")
1867
    self._InitMeta(self._FindUnusedMinor(), meta.dev_path)
1868
    if not self._IsValidMeta(meta.dev_path):
1869
      raise errors.BlockDeviceError("Cannot initalize meta device")
1870

    
1871
    if not self._AssembleLocal(self.minor, backend.dev_path, meta.dev_path):
1872
      raise errors.BlockDeviceError("Can't attach to local storage")
1873
    self._children = devices
1874

    
1875
  def RemoveChildren(self, devices):
1876
    """Detach the drbd device from local storage.
1877

1878
    """
1879
    if self.minor is None:
1880
      raise errors.BlockDeviceError("Can't attach to drbd8 during"
1881
                                    " RemoveChildren")
1882
    # early return if we don't actually have backing storage
1883
    info = self._GetDevInfo(self._GetShowData(self.minor))
1884
    if "local_dev" not in info:
1885
      return
1886
    if len(self._children) != 2:
1887
      raise errors.BlockDeviceError("We don't have two children: %s" %
1888
                                    self._children)
1889
    if self._children.count(None) == 2: # we don't actually have children :)
1890
      logger.Error("Requested detach while detached")
1891
      return
1892
    if len(devices) != 2:
1893
      raise errors.BlockDeviceError("We need two children in RemoveChildren")
1894
    for child, dev in zip(self._children, devices):
1895
      if dev != child.dev_path:
1896
        raise errors.BlockDeviceError("Mismatch in local storage"
1897
                                      " (%s != %s) in RemoveChildren" %
1898
                                      (dev, child.dev_path))
1899

    
1900
    if not self._ShutdownLocal(self.minor):
1901
      raise errors.BlockDeviceError("Can't detach from local storage")
1902
    self._children = []
1903

    
1904
  def SetSyncSpeed(self, kbytes):
1905
    """Set the speed of the DRBD syncer.
1906

1907
    """
1908
    children_result = super(DRBD8, self).SetSyncSpeed(kbytes)
1909
    if self.minor is None:
1910
      logger.Info("Instance not attached to a device")
1911
      return False
1912
    result = utils.RunCmd(["drbdsetup", self.dev_path, "syncer", "-r", "%d" %
1913
                           kbytes])
1914
    if result.failed:
1915
      logger.Error("Can't change syncer rate: %s " % result.fail_reason)
1916
    return not result.failed and children_result
1917

    
1918
  def GetSyncStatus(self):
1919
    """Returns the sync status of the device.
1920

1921
    Returns:
1922
     (sync_percent, estimated_time, is_degraded)
1923

1924
    If sync_percent is None, it means all is ok
1925
    If estimated_time is None, it means we can't esimate
1926
    the time needed, otherwise it's the time left in seconds.
1927

1928

1929
    We set the is_degraded parameter to True on two conditions:
1930
    network not connected or local disk missing.
1931

1932
    We compute the ldisk parameter based on wheter we have a local
1933
    disk or not.
1934

1935
    """
1936
    if self.minor is None and not self.Attach():
1937
      raise errors.BlockDeviceError("Can't attach to device in GetSyncStatus")
1938
    proc_info = self._MassageProcData(self._GetProcData())
1939
    if self.minor not in proc_info:
1940
      raise errors.BlockDeviceError("Can't find myself in /proc (minor %d)" %
1941
                                    self.minor)
1942
    line = proc_info[self.minor]
1943
    match = re.match("^.*sync'ed: *([0-9.]+)%.*"
1944
                     " finish: ([0-9]+):([0-9]+):([0-9]+) .*$", line)
1945
    if match:
1946
      sync_percent = float(match.group(1))
1947
      hours = int(match.group(2))
1948
      minutes = int(match.group(3))
1949
      seconds = int(match.group(4))
1950
      est_time = hours * 3600 + minutes * 60 + seconds
1951
    else:
1952
      sync_percent = None
1953
      est_time = None
1954
    match = re.match("^ *\d+: cs:(\w+).*ds:(\w+)/(\w+).*$", line)
1955
    if not match:
1956
      raise errors.BlockDeviceError("Can't find my data in /proc (minor %d)" %
1957
                                    self.minor)
1958
    client_state = match.group(1)
1959
    local_disk_state = match.group(2)
1960
    ldisk = local_disk_state != "UpToDate"
1961
    is_degraded = client_state != "Connected"
1962
    return sync_percent, est_time, is_degraded or ldisk, ldisk
1963

    
1964
  def Open(self, force=False):
1965
    """Make the local state primary.
1966

1967
    If the 'force' parameter is given, the '--do-what-I-say' parameter
1968
    is given. Since this is a pottentialy dangerous operation, the
1969
    force flag should be only given after creation, when it actually
1970
    has to be given.
1971

1972
    """
1973
    if self.minor is None and not self.Attach():
1974
      logger.Error("DRBD cannot attach to a device during open")
1975
      return False
1976
    cmd = ["drbdsetup", self.dev_path, "primary"]
1977
    if force:
1978
      cmd.append("-o")
1979
    result = utils.RunCmd(cmd)
1980
    if result.failed:
1981
      msg = ("Can't make drbd device primary: %s" % result.output)
1982
      logger.Error(msg)
1983
      raise errors.BlockDeviceError(msg)
1984

    
1985
  def Close(self):
1986
    """Make the local state secondary.
1987

1988
    This will, of course, fail if the device is in use.
1989

1990
    """
1991
    if self.minor is None and not self.Attach():
1992
      logger.Info("Instance not attached to a device")
1993
      raise errors.BlockDeviceError("Can't find device")
1994
    result = utils.RunCmd(["drbdsetup", self.dev_path, "secondary"])
1995
    if result.failed:
1996
      msg = ("Can't switch drbd device to"
1997
             " secondary: %s" % result.output)
1998
      logger.Error(msg)
1999
      raise errors.BlockDeviceError(msg)
2000

    
2001
  def Attach(self):
2002
    """Find a DRBD device which matches our config and attach to it.
2003

2004
    In case of partially attached (local device matches but no network
2005
    setup), we perform the network attach. If successful, we re-test
2006
    the attach if can return success.
2007

2008
    """
2009
    for minor in self._GetUsedDevs():
2010
      info = self._GetDevInfo(self._GetShowData(minor))
2011
      match_l = self._MatchesLocal(info)
2012
      match_r = self._MatchesNet(info)
2013
      if match_l and match_r:
2014
        break
2015
      if match_l and not match_r and "local_addr" not in info:
2016
        res_r = self._AssembleNet(minor,
2017
                                  (self._lhost, self._lport,
2018
                                   self._rhost, self._rport),
2019
                                  "C")
2020
        if res_r:
2021
          if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
2022
            break
2023
      # the weakest case: we find something that is only net attached
2024
      # even though we were passed some children at init time
2025
      if match_r and "local_dev" not in info:
2026
        break
2027
      if match_l and not match_r and "local_addr" in info:
2028
        # strange case - the device network part points to somewhere
2029
        # else, even though its local storage is ours; as we own the
2030
        # drbd space, we try to disconnect from the remote peer and
2031
        # reconnect to our correct one
2032
        if not self._ShutdownNet(minor):
2033
          raise errors.BlockDeviceError("Device has correct local storage,"
2034
                                        " wrong remote peer and is unable to"
2035
                                        " disconnect in order to attach to"
2036
                                        " the correct peer")
2037
        # note: _AssembleNet also handles the case when we don't want
2038
        # local storage (i.e. one or more of the _[lr](host|port) is
2039
        # None)
2040
        if (self._AssembleNet(minor, (self._lhost, self._lport,
2041
                                      self._rhost, self._rport), "C") and
2042
            self._MatchesNet(self._GetDevInfo(self._GetShowData(minor)))):
2043
          break
2044

    
2045
    else:
2046
      minor = None
2047

    
2048
    self._SetFromMinor(minor)
2049
    return minor is not None
2050

    
2051
  def Assemble(self):
2052
    """Assemble the drbd.
2053

2054
    Method:
2055
      - if we have a local backing device, we bind to it by:
2056
        - checking the list of used drbd devices
2057
        - check if the local minor use of any of them is our own device
2058
        - if yes, abort?
2059
        - if not, bind
2060
      - if we have a local/remote net info:
2061
        - redo the local backing device step for the remote device
2062
        - check if any drbd device is using the local port,
2063
          if yes abort
2064
        - check if any remote drbd device is using the remote
2065
          port, if yes abort (for now)
2066
        - bind our net port
2067
        - bind the remote net port
2068

2069
    """
2070
    self.Attach()
2071
    if self.minor is not None:
2072
      logger.Info("Already assembled")
2073
      return True
2074

    
2075
    result = super(DRBD8, self).Assemble()
2076
    if not result:
2077
      return result
2078

    
2079
    minor = self._FindUnusedMinor()
2080
    need_localdev_teardown = False
2081
    if self._children and self._children[0] and self._children[1]:
2082
      result = self._AssembleLocal(minor, self._children[0].dev_path,
2083
                                   self._children[1].dev_path)
2084
      if not result:
2085
        return False
2086
      need_localdev_teardown = True
2087
    if self._lhost and self._lport and self._rhost and self._rport:
2088
      result = self._AssembleNet(minor,
2089
                                 (self._lhost, self._lport,
2090
                                  self._rhost, self._rport),
2091
                                 "C")
2092
      if not result:
2093
        if need_localdev_teardown:
2094
          # we will ignore failures from this
2095
          logger.Error("net setup failed, tearing down local device")
2096
          self._ShutdownAll(minor)
2097
        return False
2098
    self._SetFromMinor(minor)
2099
    return True
2100

    
2101
  @classmethod
2102
  def _ShutdownLocal(cls, minor):
2103
    """Detach from the local device.
2104

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

2108
    """
2109
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "detach"])
2110
    if result.failed:
2111
      logger.Error("Can't detach local device: %s" % result.output)
2112
    return not result.failed
2113

    
2114
  @classmethod
2115
  def _ShutdownNet(cls, minor):
2116
    """Disconnect from the remote peer.
2117

2118
    This fails if we don't have a local device.
2119

2120
    """
2121
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disconnect"])
2122
    if result.failed:
2123
      logger.Error("Can't shutdown network: %s" % result.output)
2124
    return not result.failed
2125

    
2126
  @classmethod
2127
  def _ShutdownAll(cls, minor):
2128
    """Deactivate the device.
2129

2130
    This will, of course, fail if the device is in use.
2131

2132
    """
2133
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "down"])
2134
    if result.failed:
2135
      logger.Error("Can't shutdown drbd device: %s" % result.output)
2136
    return not result.failed
2137

    
2138
  def Shutdown(self):
2139
    """Shutdown the DRBD device.
2140

2141
    """
2142
    if self.minor is None and not self.Attach():
2143
      logger.Info("DRBD device not attached to a device during Shutdown")
2144
      return True
2145
    if not self._ShutdownAll(self.minor):
2146
      return False
2147
    self.minor = None
2148
    self.dev_path = None
2149
    return True
2150

    
2151
  def Remove(self):
2152
    """Stub remove for DRBD devices.
2153

2154
    """
2155
    return self.Shutdown()
2156

    
2157
  @classmethod
2158
  def Create(cls, unique_id, children, size):
2159
    """Create a new DRBD8 device.
2160

2161
    Since DRBD devices are not created per se, just assembled, this
2162
    function only initializes the metadata.
2163

2164
    """
2165
    if len(children) != 2:
2166
      raise errors.ProgrammerError("Invalid setup for the drbd device")
2167
    meta = children[1]
2168
    meta.Assemble()
2169
    if not meta.Attach():
2170
      raise errors.BlockDeviceError("Can't attach to meta device")
2171
    if not cls._CheckMetaSize(meta.dev_path):
2172
      raise errors.BlockDeviceError("Invalid meta device size")
2173
    cls._InitMeta(cls._FindUnusedMinor(), meta.dev_path)
2174
    if not cls._IsValidMeta(meta.dev_path):
2175
      raise errors.BlockDeviceError("Cannot initalize meta device")
2176
    return cls(unique_id, children)
2177

    
2178

    
2179
class FileStorage(BlockDev):
2180
  """File device.
2181
  
2182
  This class represents the a file storage backend device.
2183

2184
  The unique_id for the file device is a (file_driver, file_path) tuple.
2185
  
2186
  """
2187
  def __init__(self, unique_id, children):
2188
    """Initalizes a file device backend.
2189

2190
    """
2191
    if children:
2192
      raise errors.BlockDeviceError("Invalid setup for file device")
2193
    super(FileStorage, self).__init__(unique_id, children)
2194
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
2195
      raise ValueError("Invalid configuration data %s" % str(unique_id))
2196
    self.driver = unique_id[0]
2197
    self.dev_path = unique_id[1]
2198

    
2199
  def Assemble(self):
2200
    """Assemble the device.
2201

2202
    Checks whether the file device exists, raises BlockDeviceError otherwise.
2203

2204
    """
2205
    if not os.path.exists(self.dev_path):
2206
      raise errors.BlockDeviceError("File device '%s' does not exist." %
2207
                                    self.dev_path)
2208
    return True
2209

    
2210
  def Shutdown(self):
2211
    """Shutdown the device.
2212

2213
    This is a no-op for the file type, as we don't deacivate
2214
    the file on shutdown.
2215

2216
    """
2217
    return True
2218

    
2219
  def Open(self, force=False):
2220
    """Make the device ready for I/O.
2221

2222
    This is a no-op for the file type.
2223

2224
    """
2225
    pass
2226

    
2227
  def Close(self):
2228
    """Notifies that the device will no longer be used for I/O.
2229

2230
    This is a no-op for the file type.
2231

2232
    """
2233
    pass
2234

    
2235
  def Remove(self):
2236
    """Remove the file backing the block device.
2237

2238
    Returns:
2239
      boolean indicating wheter removal of file was successful or not.
2240

2241
    """
2242
    if not os.path.exists(self.dev_path):
2243
      return True
2244
    try:
2245
      os.remove(self.dev_path)
2246
      return True
2247
    except OSError, err:
2248
      logger.Error("Can't remove file '%s': %s"
2249
                   % (self.dev_path, err))
2250
      return False
2251

    
2252
  def Attach(self):
2253
    """Attach to an existing file.
2254

2255
    Check if this file already exists.
2256

2257
    Returns:
2258
      boolean indicating if file exists or not.
2259

2260
    """
2261
    if os.path.exists(self.dev_path):
2262
      return True
2263
    return False
2264

    
2265
  @classmethod
2266
  def Create(cls, unique_id, children, size):
2267
    """Create a new file.
2268

2269
    Args:
2270
      children:
2271
      size: integer size of file in MiB
2272

2273
    Returns:
2274
      A ganeti.bdev.FileStorage object.
2275

2276
    """
2277
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
2278
      raise ValueError("Invalid configuration data %s" % str(unique_id))
2279
    dev_path = unique_id[1]
2280
    try:
2281
      f = open(dev_path, 'w')
2282
    except IOError, err:
2283
      raise errors.BlockDeviceError("Could not create '%'" % err)
2284
    else:
2285
      f.truncate(size * 1024 * 1024)
2286
      f.close()
2287

    
2288
    return FileStorage(unique_id, children)
2289

    
2290

    
2291
DEV_MAP = {
2292
  constants.LD_LV: LogicalVolume,
2293
  constants.LD_MD_R1: MDRaid1,
2294
  constants.LD_DRBD7: DRBDev,
2295
  constants.LD_DRBD8: DRBD8,
2296
  constants.LD_FILE: FileStorage,
2297
  }
2298

    
2299

    
2300
def FindDevice(dev_type, unique_id, children):
2301
  """Search for an existing, assembled device.
2302

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

2306
  """
2307
  if dev_type not in DEV_MAP:
2308
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
2309
  device = DEV_MAP[dev_type](unique_id, children)
2310
  if not device.Attach():
2311
    return None
2312
  return  device
2313

    
2314

    
2315
def AttachOrAssemble(dev_type, unique_id, children):
2316
  """Try to attach or assemble an existing device.
2317

2318
  This will attach to an existing assembled device or will assemble
2319
  the device, as needed, to bring it fully up.
2320

2321
  """
2322
  if dev_type not in DEV_MAP:
2323
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
2324
  device = DEV_MAP[dev_type](unique_id, children)
2325
  if not device.Attach():
2326
    device.Assemble()
2327
    if not device.Attach():
2328
      raise errors.BlockDeviceError("Can't find a valid block device for"
2329
                                    " %s/%s/%s" %
2330
                                    (dev_type, unique_id, children))
2331
  return device
2332

    
2333

    
2334
def Create(dev_type, unique_id, children, size):
2335
  """Create a device.
2336

2337
  """
2338
  if dev_type not in DEV_MAP:
2339
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
2340
  device = DEV_MAP[dev_type].Create(unique_id, children, size)
2341
  return device