Statistics
| Branch: | Tag: | Revision:

root / lib / bdev.py @ 1063abd1

History | View | Annotate | Download (52.9 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
def _IgnoreError(fn, *args, **kwargs):
37
  """Executes the given function, ignoring BlockDeviceErrors.
38

39
  This is used in order to simplify the execution of cleanup or
40
  rollback functions.
41

42
  @rtype: boolean
43
  @return: True when fn didn't raise an exception, False otherwise
44

45
  """
46
  try:
47
    fn(*args, **kwargs)
48
    return True
49
  except errors.BlockDeviceError, err:
50
    logging.warning("Caught BlockDeviceError but ignoring: %s" % str(err))
51
    return False
52

    
53

    
54
def _ThrowError(msg, *args):
55
  """Log an error to the node daemon and the raise an exception.
56

57
  @type msg: string
58
  @param msg: the text of the exception
59
  @raise errors.BlockDeviceError
60

61
  """
62
  if args:
63
    msg = msg % args
64
  logging.error(msg)
65
  raise errors.BlockDeviceError(msg)
66

    
67

    
68
class BlockDev(object):
69
  """Block device abstract class.
70

71
  A block device can be in the following states:
72
    - not existing on the system, and by `Create()` it goes into:
73
    - existing but not setup/not active, and by `Assemble()` goes into:
74
    - active read-write and by `Open()` it goes into
75
    - online (=used, or ready for use)
76

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

83
  The many different states of the device are due to the fact that we
84
  need to cover many device types:
85
    - logical volumes are created, lvchange -a y $lv, and used
86
    - drbd devices are attached to a local disk/remote peer and made primary
87

88
  A block device is identified by three items:
89
    - the /dev path of the device (dynamic)
90
    - a unique ID of the device (static)
91
    - it's major/minor pair (dynamic)
92

93
  Not all devices implement both the first two as distinct items. LVM
94
  logical volumes have their unique ID (the pair volume group, logical
95
  volume name) in a 1-to-1 relation to the dev path. For DRBD devices,
96
  the /dev path is again dynamic and the unique id is the pair (host1,
97
  dev1), (host2, dev2).
98

99
  You can get to a device in two ways:
100
    - creating the (real) device, which returns you
101
      an attached instance (lvcreate)
102
    - attaching of a python instance to an existing (real) device
103

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

110
  """
111
  def __init__(self, unique_id, children):
112
    self._children = children
113
    self.dev_path = None
114
    self.unique_id = unique_id
115
    self.major = None
116
    self.minor = None
117
    self.attached = False
118

    
119
  def Assemble(self):
120
    """Assemble the device from its components.
121

122
    Implementations of this method by child classes must ensure that:
123
      - after the device has been assembled, it knows its major/minor
124
        numbers; this allows other devices (usually parents) to probe
125
        correctly for their children
126
      - calling this method on an existing, in-use device is safe
127
      - if the device is already configured (and in an OK state),
128
        this method is idempotent
129

130
    """
131
    pass
132

    
133
  def Attach(self):
134
    """Find a device which matches our config and attach to it.
135

136
    """
137
    raise NotImplementedError
138

    
139
  def Close(self):
140
    """Notifies that the device will no longer be used for I/O.
141

142
    """
143
    raise NotImplementedError
144

    
145
  @classmethod
146
  def Create(cls, unique_id, children, size):
147
    """Create the device.
148

149
    If the device cannot be created, it will return None
150
    instead. Error messages go to the logging system.
151

152
    Note that for some devices, the unique_id is used, and for other,
153
    the children. The idea is that these two, taken together, are
154
    enough for both creation and assembly (later).
155

156
    """
157
    raise NotImplementedError
158

    
159
  def Remove(self):
160
    """Remove this device.
161

162
    This makes sense only for some of the device types: LV and file
163
    storeage. Also note that if the device can't attach, the removal
164
    can't be completed.
165

166
    """
167
    raise NotImplementedError
168

    
169
  def Rename(self, new_id):
170
    """Rename this device.
171

172
    This may or may not make sense for a given device type.
173

174
    """
175
    raise NotImplementedError
176

    
177
  def Open(self, force=False):
178
    """Make the device ready for use.
179

180
    This makes the device ready for I/O. For now, just the DRBD
181
    devices need this.
182

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

186
    """
187
    raise NotImplementedError
188

    
189
  def Shutdown(self):
190
    """Shut down the device, freeing its children.
191

192
    This undoes the `Assemble()` work, except for the child
193
    assembling; as such, the children on the device are still
194
    assembled after this call.
195

196
    """
197
    raise NotImplementedError
198

    
199
  def SetSyncSpeed(self, speed):
200
    """Adjust the sync speed of the mirror.
201

202
    In case this is not a mirroring device, this is no-op.
203

204
    """
205
    result = True
206
    if self._children:
207
      for child in self._children:
208
        result = result and child.SetSyncSpeed(speed)
209
    return result
210

    
211
  def GetSyncStatus(self):
212
    """Returns the sync status of the device.
213

214
    If this device is a mirroring device, this function returns the
215
    status of the mirror.
216

217
    If sync_percent is None, it means the device is not syncing.
218

219
    If estimated_time is None, it means we can't estimate
220
    the time needed, otherwise it's the time left in seconds.
221

222
    If is_degraded is True, it means the device is missing
223
    redundancy. This is usually a sign that something went wrong in
224
    the device setup, if sync_percent is None.
225

226
    The ldisk parameter represents the degradation of the local
227
    data. This is only valid for some devices, the rest will always
228
    return False (not degraded).
229

230
    @rtype: tuple
231
    @return: (sync_percent, estimated_time, is_degraded, ldisk)
232

233
    """
234
    return None, None, False, False
235

    
236

    
237
  def CombinedSyncStatus(self):
238
    """Calculate the mirror status recursively for our children.
239

240
    The return value is the same as for `GetSyncStatus()` except the
241
    minimum percent and maximum time are calculated across our
242
    children.
243

244
    """
245
    min_percent, max_time, is_degraded, ldisk = self.GetSyncStatus()
246
    if self._children:
247
      for child in self._children:
248
        c_percent, c_time, c_degraded, c_ldisk = child.GetSyncStatus()
249
        if min_percent is None:
250
          min_percent = c_percent
251
        elif c_percent is not None:
252
          min_percent = min(min_percent, c_percent)
253
        if max_time is None:
254
          max_time = c_time
255
        elif c_time is not None:
256
          max_time = max(max_time, c_time)
257
        is_degraded = is_degraded or c_degraded
258
        ldisk = ldisk or c_ldisk
259
    return min_percent, max_time, is_degraded, ldisk
260

    
261

    
262
  def SetInfo(self, text):
263
    """Update metadata with info text.
264

265
    Only supported for some device types.
266

267
    """
268
    for child in self._children:
269
      child.SetInfo(text)
270

    
271
  def Grow(self, amount):
272
    """Grow the block device.
273

274
    @param amount: the amount (in mebibytes) to grow with
275

276
    """
277
    raise NotImplementedError
278

    
279
  def __repr__(self):
280
    return ("<%s: unique_id: %s, children: %s, %s:%s, %s>" %
281
            (self.__class__, self.unique_id, self._children,
282
             self.major, self.minor, self.dev_path))
283

    
284

    
285
class LogicalVolume(BlockDev):
286
  """Logical Volume block device.
287

288
  """
289
  def __init__(self, unique_id, children):
290
    """Attaches to a LV device.
291

292
    The unique_id is a tuple (vg_name, lv_name)
293

294
    """
295
    super(LogicalVolume, self).__init__(unique_id, children)
296
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
297
      raise ValueError("Invalid configuration data %s" % str(unique_id))
298
    self._vg_name, self._lv_name = unique_id
299
    self.dev_path = "/dev/%s/%s" % (self._vg_name, self._lv_name)
300
    self._degraded = True
301
    self.major = self.minor = None
302
    self.Attach()
303

    
304
  @classmethod
305
  def Create(cls, unique_id, children, size):
306
    """Create a new logical volume.
307

308
    """
309
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
310
      raise errors.ProgrammerError("Invalid configuration data %s" %
311
                                   str(unique_id))
312
    vg_name, lv_name = unique_id
313
    pvs_info = cls.GetPVInfo(vg_name)
314
    if not pvs_info:
315
      _ThrowError("Can't compute PV info for vg %s", vg_name)
316
    pvs_info.sort()
317
    pvs_info.reverse()
318

    
319
    pvlist = [ pv[1] for pv in pvs_info ]
320
    free_size = sum([ pv[0] for pv in pvs_info ])
321

    
322
    # The size constraint should have been checked from the master before
323
    # calling the create function.
324
    if free_size < size:
325
      _ThrowError("Not enough free space: required %s,"
326
                  " available %s", size, free_size)
327
    result = utils.RunCmd(["lvcreate", "-L%dm" % size, "-n%s" % lv_name,
328
                           vg_name] + pvlist)
329
    if result.failed:
330
      _ThrowError("LV create failed (%s): %s",
331
                  result.fail_reason, result.output)
332
    return LogicalVolume(unique_id, children)
333

    
334
  @staticmethod
335
  def GetPVInfo(vg_name):
336
    """Get the free space info for PVs in a volume group.
337

338
    @param vg_name: the volume group name
339

340
    @rtype: list
341
    @return: list of tuples (free_space, name) with free_space in mebibytes
342

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

    
363
    return data
364

    
365
  def Remove(self):
366
    """Remove this logical volume.
367

368
    """
369
    if not self.minor and not self.Attach():
370
      # the LV does not exist
371
      return
372
    result = utils.RunCmd(["lvremove", "-f", "%s/%s" %
373
                           (self._vg_name, self._lv_name)])
374
    if result.failed:
375
      _ThrowError("Can't lvremove: %s - %s", result.fail_reason, result.output)
376

    
377
  def Rename(self, new_id):
378
    """Rename this logical volume.
379

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

    
394
  def Attach(self):
395
    """Attach to an existing LV.
396

397
    This method will try to see if an existing and active LV exists
398
    which matches our name. If so, its major/minor will be
399
    recorded.
400

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

    
416
    status, major, minor = out[:3]
417
    if len(status) != 6:
418
      logging.error("lvs lv_attr is not 6 characters (%s)", status)
419
      return False
420

    
421
    try:
422
      major = int(major)
423
      minor = int(minor)
424
    except ValueError, err:
425
      logging.error("lvs major/minor cannot be parsed: %s", str(err))
426

    
427
    self.major = major
428
    self.minor = minor
429
    self._degraded = status[0] == 'v' # virtual volume, i.e. doesn't backing
430
                                      # storage
431
    self.attached = True
432
    return True
433

    
434
  def Assemble(self):
435
    """Assemble the device.
436

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

441
    """
442
    result = utils.RunCmd(["lvchange", "-ay", self.dev_path])
443
    if result.failed:
444
      _ThrowError("Can't activate lv %s: %s", self.dev_path, result.output)
445

    
446
  def Shutdown(self):
447
    """Shutdown the device.
448

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

452
    """
453
    pass
454

    
455
  def GetSyncStatus(self):
456
    """Returns the sync status of the device.
457

458
    If this device is a mirroring device, this function returns the
459
    status of the mirror.
460

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

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

472
    The status was already read in Attach, so we just return it.
473

474
    @rtype: tuple
475
    @return: (sync_percent, estimated_time, is_degraded, ldisk)
476

477
    """
478
    return None, None, self._degraded, self._degraded
479

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

483
    This is a no-op for the LV device type.
484

485
    """
486
    pass
487

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

491
    This is a no-op for the LV device type.
492

493
    """
494
    pass
495

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

499
    """
500
    snap_name = self._lv_name + ".snap"
501

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

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

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

    
522
    return snap_name
523

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

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

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

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

    
537
    result = utils.RunCmd(["lvchange", "--addtag", text,
538
                           self.dev_path])
539
    if result.failed:
540
      _ThrowError("Command: %s error: %s - %s", result.cmd, result.fail_reason,
541
                  result.output)
542

    
543
  def Grow(self, amount):
544
    """Grow the logical volume.
545

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

    
558

    
559
class DRBD8Status(object):
560
  """A DRBD status representation class.
561

562
  Note that this doesn't support unconfigured devices (cs:Unconfigured).
563

564
  """
565
  UNCONF_RE = re.compile(r"\s*[0-9]+:\s*cs:Unconfigured$")
566
  LINE_RE = re.compile(r"\s*[0-9]+:\s*cs:(\S+)\s+st:([^/]+)/(\S+)"
567
                       "\s+ds:([^/]+)/(\S+)\s+.*$")
568
  SYNC_RE = re.compile(r"^.*\ssync'ed:\s*([0-9.]+)%.*"
569
                       "\sfinish: ([0-9]+):([0-9]+):([0-9]+)\s.*$")
570

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

    
586
    # end reading of data from the LINE_RE or UNCONF_RE
587

    
588
    self.is_standalone = self.cstatus == "StandAlone"
589
    self.is_wfconn = self.cstatus == "WFConnection"
590
    self.is_connected = self.cstatus == "Connected"
591
    self.is_primary = self.lrole == "Primary"
592
    self.is_secondary = self.lrole == "Secondary"
593
    self.peer_primary = self.rrole == "Primary"
594
    self.peer_secondary = self.rrole == "Secondary"
595
    self.both_primary = self.is_primary and self.peer_primary
596
    self.both_secondary = self.is_secondary and self.peer_secondary
597

    
598
    self.is_diskless = self.ldisk == "Diskless"
599
    self.is_disk_uptodate = self.ldisk == "UpToDate"
600

    
601
    self.is_in_resync = self.cstatus in ("SyncSource", "SyncTarget")
602
    self.is_in_use = self.cstatus != "Unconfigured"
603

    
604
    m = self.SYNC_RE.match(procline)
605
    if m:
606
      self.sync_percent = float(m.group(1))
607
      hours = int(m.group(2))
608
      minutes = int(m.group(3))
609
      seconds = int(m.group(4))
610
      self.est_time = hours * 3600 + minutes * 60 + seconds
611
    else:
612
      self.sync_percent = None
613
      self.est_time = None
614

    
615
    self.is_sync_target = self.peer_sync_source = self.cstatus == "SyncTarget"
616
    self.peer_sync_target = self.is_sync_source = self.cstatus == "SyncSource"
617
    self.is_resync = self.is_sync_target or self.is_sync_source
618

    
619

    
620
class BaseDRBD(BlockDev):
621
  """Base DRBD class.
622

623
  This class contains a few bits of common functionality between the
624
  0.7 and 8.x versions of DRBD.
625

626
  """
627
  _VERSION_RE = re.compile(r"^version: (\d+)\.(\d+)\.(\d+)"
628
                           r" \(api:(\d+)/proto:(\d+)(?:-(\d+))?\)")
629

    
630
  _DRBD_MAJOR = 147
631
  _ST_UNCONFIGURED = "Unconfigured"
632
  _ST_WFCONNECTION = "WFConnection"
633
  _ST_CONNECTED = "Connected"
634

    
635
  _STATUS_FILE = "/proc/drbd"
636

    
637
  @staticmethod
638
  def _GetProcData(filename=_STATUS_FILE):
639
    """Return data from /proc/drbd.
640

641
    """
642
    stat = open(filename, "r")
643
    try:
644
      data = stat.read().splitlines()
645
    finally:
646
      stat.close()
647
    if not data:
648
      _ThrowError("Can't read any data from %s", filename)
649
    return data
650

    
651
  @staticmethod
652
  def _MassageProcData(data):
653
    """Transform the output of _GetProdData into a nicer form.
654

655
    @return: a dictionary of minor: joined lines from /proc/drbd
656
        for that minor
657

658
    """
659
    lmatch = re.compile("^ *([0-9]+):.*$")
660
    results = {}
661
    old_minor = old_line = None
662
    for line in data:
663
      lresult = lmatch.match(line)
664
      if lresult is not None:
665
        if old_minor is not None:
666
          results[old_minor] = old_line
667
        old_minor = int(lresult.group(1))
668
        old_line = line
669
      else:
670
        if old_minor is not None:
671
          old_line += " " + line.strip()
672
    # add last line
673
    if old_minor is not None:
674
      results[old_minor] = old_line
675
    return results
676

    
677
  @classmethod
678
  def _GetVersion(cls):
679
    """Return the DRBD version.
680

681
    This will return a dict with keys:
682
      - k_major
683
      - k_minor
684
      - k_point
685
      - api
686
      - proto
687
      - proto2 (only on drbd > 8.2.X)
688

689
    """
690
    proc_data = cls._GetProcData()
691
    first_line = proc_data[0].strip()
692
    version = cls._VERSION_RE.match(first_line)
693
    if not version:
694
      raise errors.BlockDeviceError("Can't parse DRBD version from '%s'" %
695
                                    first_line)
696

    
697
    values = version.groups()
698
    retval = {'k_major': int(values[0]),
699
              'k_minor': int(values[1]),
700
              'k_point': int(values[2]),
701
              'api': int(values[3]),
702
              'proto': int(values[4]),
703
             }
704
    if values[5] is not None:
705
      retval['proto2'] = values[5]
706

    
707
    return retval
708

    
709
  @staticmethod
710
  def _DevPath(minor):
711
    """Return the path to a drbd device for a given minor.
712

713
    """
714
    return "/dev/drbd%d" % minor
715

    
716
  @classmethod
717
  def GetUsedDevs(cls):
718
    """Compute the list of used DRBD devices.
719

720
    """
721
    data = cls._GetProcData()
722

    
723
    used_devs = {}
724
    valid_line = re.compile("^ *([0-9]+): cs:([^ ]+).*$")
725
    for line in data:
726
      match = valid_line.match(line)
727
      if not match:
728
        continue
729
      minor = int(match.group(1))
730
      state = match.group(2)
731
      if state == cls._ST_UNCONFIGURED:
732
        continue
733
      used_devs[minor] = state, line
734

    
735
    return used_devs
736

    
737
  def _SetFromMinor(self, minor):
738
    """Set our parameters based on the given minor.
739

740
    This sets our minor variable and our dev_path.
741

742
    """
743
    if minor is None:
744
      self.minor = self.dev_path = None
745
      self.attached = False
746
    else:
747
      self.minor = minor
748
      self.dev_path = self._DevPath(minor)
749
      self.attached = True
750

    
751
  @staticmethod
752
  def _CheckMetaSize(meta_device):
753
    """Check if the given meta device looks like a valid one.
754

755
    This currently only check the size, which must be around
756
    128MiB.
757

758
    """
759
    result = utils.RunCmd(["blockdev", "--getsize", meta_device])
760
    if result.failed:
761
      logging.error("Failed to get device size: %s - %s",
762
                    result.fail_reason, result.output)
763
      return False
764
    try:
765
      sectors = int(result.stdout)
766
    except ValueError:
767
      logging.error("Invalid output from blockdev: '%s'", result.stdout)
768
      return False
769
    bytes = sectors * 512
770
    if bytes < 128 * 1024 * 1024: # less than 128MiB
771
      logging.error("Meta device too small (%.2fMib)", (bytes / 1024 / 1024))
772
      return False
773
    if bytes > (128 + 32) * 1024 * 1024: # account for an extra (big) PE on LVM
774
      logging.error("Meta device too big (%.2fMiB)", (bytes / 1024 / 1024))
775
      return False
776
    return True
777

    
778
  def Rename(self, new_id):
779
    """Rename a device.
780

781
    This is not supported for drbd devices.
782

783
    """
784
    raise errors.ProgrammerError("Can't rename a drbd device")
785

    
786

    
787
class DRBD8(BaseDRBD):
788
  """DRBD v8.x block device.
789

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

794
  The unique_id for the drbd device is the (local_ip, local_port,
795
  remote_ip, remote_port) tuple, and it must have two children: the
796
  data device and the meta_device. The meta device is checked for
797
  valid size and is zeroed on create.
798

799
  """
800
  _MAX_MINORS = 255
801
  _PARSE_SHOW = None
802

    
803
  # timeout constants
804
  _NET_RECONFIG_TIMEOUT = 60
805

    
806
  def __init__(self, unique_id, children):
807
    if children and children.count(None) > 0:
808
      children = []
809
    super(DRBD8, self).__init__(unique_id, children)
810
    self.major = self._DRBD_MAJOR
811
    version = self._GetVersion()
812
    if version['k_major'] != 8 :
813
      _ThrowError("Mismatch in DRBD kernel version and requested ganeti"
814
                  " usage: kernel is %s.%s, ganeti wants 8.x",
815
                  version['k_major'], version['k_minor'])
816

    
817
    if len(children) not in (0, 2):
818
      raise ValueError("Invalid configuration data %s" % str(children))
819
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 6:
820
      raise ValueError("Invalid configuration data %s" % str(unique_id))
821
    (self._lhost, self._lport,
822
     self._rhost, self._rport,
823
     self._aminor, self._secret) = unique_id
824
    if (self._lhost is not None and self._lhost == self._rhost and
825
        self._lport == self._rport):
826
      raise ValueError("Invalid configuration data, same local/remote %s" %
827
                       (unique_id,))
828
    self.Attach()
829

    
830
  @classmethod
831
  def _InitMeta(cls, minor, dev_path):
832
    """Initialize a meta device.
833

834
    This will not work if the given minor is in use.
835

836
    """
837
    result = utils.RunCmd(["drbdmeta", "--force", cls._DevPath(minor),
838
                           "v08", dev_path, "0", "create-md"])
839
    if result.failed:
840
      _ThrowError("Can't initialize meta device: %s", result.output)
841

    
842
  @classmethod
843
  def _FindUnusedMinor(cls):
844
    """Find an unused DRBD device.
845

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

849
    """
850
    data = cls._GetProcData()
851

    
852
    unused_line = re.compile("^ *([0-9]+): cs:Unconfigured$")
853
    used_line = re.compile("^ *([0-9]+): cs:")
854
    highest = None
855
    for line in data:
856
      match = unused_line.match(line)
857
      if match:
858
        return int(match.group(1))
859
      match = used_line.match(line)
860
      if match:
861
        minor = int(match.group(1))
862
        highest = max(highest, minor)
863
    if highest is None: # there are no minors in use at all
864
      return 0
865
    if highest >= cls._MAX_MINORS:
866
      logging.error("Error: no free drbd minors!")
867
      raise errors.BlockDeviceError("Can't find a free DRBD minor")
868
    return highest + 1
869

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

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

877
    """
878
    if cls._PARSE_SHOW is not None:
879
      return cls._PARSE_SHOW
880

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

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

    
892
    keyword = pyp.Word(pyp.alphanums + '-')
893

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

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

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

    
913
    bnf = pyp.ZeroOrMore(pyp.Group(section ^ stmt))
914
    bnf.ignore(comment)
915

    
916
    cls._PARSE_SHOW = bnf
917

    
918
    return bnf
919

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

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

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

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

941
    """
942
    data = {}
943
    if not out:
944
      return data
945

    
946
    bnf = cls._GetShowParser()
947
    # run pyparse
948

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1030
    """
1031
    args = ["drbdsetup", cls._DevPath(minor), "disk",
1032
            backend, meta, "0", "-e", "detach", "--create-device"]
1033
    result = utils.RunCmd(args)
1034
    if result.failed:
1035
      _ThrowError("drbd%d: can't attach local disk: %s", minor, result.output)
1036

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

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

    
1050
    # Workaround for a race condition. When DRBD is doing its dance to
1051
    # establish a connection with its peer, it also sends the
1052
    # synchronization speed over the wire. In some cases setting the
1053
    # sync speed only after setting up both sides can race with DRBD
1054
    # connecting, hence we set it here before telling DRBD anything
1055
    # about its peer.
1056
    cls._SetMinorSyncSpeed(minor, constants.SYNC_SPEED)
1057

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

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

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

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

    
1110
    self._AssembleLocal(self.minor, backend.dev_path, meta.dev_path)
1111
    self._children = devices
1112

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

1116
    """
1117
    if self.minor is None:
1118
      _ThrowError("drbd%d: can't attach to drbd8 during RemoveChildren",
1119
                  self._aminor)
1120
    # early return if we don't actually have backing storage
1121
    info = self._GetDevInfo(self._GetShowData(self.minor))
1122
    if "local_dev" not in info:
1123
      return
1124
    if len(self._children) != 2:
1125
      _ThrowError("drbd%d: we don't have two children: %s", self.minor,
1126
                  self._children)
1127
    if self._children.count(None) == 2: # we don't actually have children :)
1128
      logging.warning("drbd%d: requested detach while detached", self.minor)
1129
      return
1130
    if len(devices) != 2:
1131
      _ThrowError("drbd%d: we need two children in RemoveChildren", self.minor)
1132
    for child, dev in zip(self._children, devices):
1133
      if dev != child.dev_path:
1134
        _ThrowError("drbd%d: mismatch in local storage (%s != %s) in"
1135
                    " RemoveChildren", self.minor, dev, child.dev_path)
1136

    
1137
    self._ShutdownLocal(self.minor)
1138
    self._children = []
1139

    
1140
  @classmethod
1141
  def _SetMinorSyncSpeed(cls, minor, kbytes):
1142
    """Set the speed of the DRBD syncer.
1143

1144
    This is the low-level implementation.
1145

1146
    @type minor: int
1147
    @param minor: the drbd minor whose settings we change
1148
    @type kbytes: int
1149
    @param kbytes: the speed in kbytes/second
1150
    @rtype: boolean
1151
    @return: the success of the operation
1152

1153
    """
1154
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "syncer",
1155
                           "-r", "%d" % kbytes, "--create-device"])
1156
    if result.failed:
1157
      logging.error("Can't change syncer rate: %s - %s",
1158
                    result.fail_reason, result.output)
1159
    return not result.failed
1160

    
1161
  def SetSyncSpeed(self, kbytes):
1162
    """Set the speed of the DRBD syncer.
1163

1164
    @type kbytes: int
1165
    @param kbytes: the speed in kbytes/second
1166
    @rtype: boolean
1167
    @return: the success of the operation
1168

1169
    """
1170
    if self.minor is None:
1171
      logging.info("Not attached during SetSyncSpeed")
1172
      return False
1173
    children_result = super(DRBD8, self).SetSyncSpeed(kbytes)
1174
    return self._SetMinorSyncSpeed(self.minor, kbytes) and children_result
1175

    
1176
  def GetProcStatus(self):
1177
    """Return device data from /proc.
1178

1179
    """
1180
    if self.minor is None:
1181
      _ThrowError("drbd%d: GetStats() called while not attached", self._aminor)
1182
    proc_info = self._MassageProcData(self._GetProcData())
1183
    if self.minor not in proc_info:
1184
      _ThrowError("drbd%d: can't find myself in /proc", self.minor)
1185
    return DRBD8Status(proc_info[self.minor])
1186

    
1187
  def GetSyncStatus(self):
1188
    """Returns the sync status of the device.
1189

1190

1191
    If sync_percent is None, it means all is ok
1192
    If estimated_time is None, it means we can't esimate
1193
    the time needed, otherwise it's the time left in seconds.
1194

1195

1196
    We set the is_degraded parameter to True on two conditions:
1197
    network not connected or local disk missing.
1198

1199
    We compute the ldisk parameter based on wheter we have a local
1200
    disk or not.
1201

1202
    @rtype: tuple
1203
    @return: (sync_percent, estimated_time, is_degraded, ldisk)
1204

1205
    """
1206
    if self.minor is None and not self.Attach():
1207
      _ThrowError("drbd%d: can't Attach() in GetSyncStatus", self._aminor)
1208
    stats = self.GetProcStatus()
1209
    ldisk = not stats.is_disk_uptodate
1210
    is_degraded = not stats.is_connected
1211
    return stats.sync_percent, stats.est_time, is_degraded or ldisk, ldisk
1212

    
1213
  def Open(self, force=False):
1214
    """Make the local state primary.
1215

1216
    If the 'force' parameter is given, the '-o' option is passed to
1217
    drbdsetup. Since this is a potentially dangerous operation, the
1218
    force flag should be only given after creation, when it actually
1219
    is mandatory.
1220

1221
    """
1222
    if self.minor is None and not self.Attach():
1223
      logging.error("DRBD cannot attach to a device during open")
1224
      return False
1225
    cmd = ["drbdsetup", self.dev_path, "primary"]
1226
    if force:
1227
      cmd.append("-o")
1228
    result = utils.RunCmd(cmd)
1229
    if result.failed:
1230
      _ThrowError("drbd%d: can't make drbd device primary: %s", self.minor,
1231
                  result.output)
1232

    
1233
  def Close(self):
1234
    """Make the local state secondary.
1235

1236
    This will, of course, fail if the device is in use.
1237

1238
    """
1239
    if self.minor is None and not self.Attach():
1240
      _ThrowError("drbd%d: can't Attach() in Close()", self._aminor)
1241
    result = utils.RunCmd(["drbdsetup", self.dev_path, "secondary"])
1242
    if result.failed:
1243
      _ThrowError("drbd%d: can't switch drbd device to secondary: %s",
1244
                  self.minor, result.output)
1245

    
1246
  def DisconnectNet(self):
1247
    """Removes network configuration.
1248

1249
    This method shutdowns the network side of the device.
1250

1251
    The method will wait up to a hardcoded timeout for the device to
1252
    go into standalone after the 'disconnect' command before
1253
    re-configuring it, as sometimes it takes a while for the
1254
    disconnect to actually propagate and thus we might issue a 'net'
1255
    command while the device is still connected. If the device will
1256
    still be attached to the network and we time out, we raise an
1257
    exception.
1258

1259
    """
1260
    if self.minor is None:
1261
      _ThrowError("drbd%d: disk not attached in re-attach net", self._aminor)
1262

    
1263
    if None in (self._lhost, self._lport, self._rhost, self._rport):
1264
      _ThrowError("drbd%d: DRBD disk missing network info in"
1265
                  " DisconnectNet()", self.minor)
1266

    
1267
    ever_disconnected = _IgnoreError(self._ShutdownNet, self.minor)
1268
    timeout_limit = time.time() + self._NET_RECONFIG_TIMEOUT
1269
    sleep_time = 0.100 # we start the retry time at 100 miliseconds
1270
    while time.time() < timeout_limit:
1271
      status = self.GetProcStatus()
1272
      if status.is_standalone:
1273
        break
1274
      # retry the disconnect, it seems possible that due to a
1275
      # well-time disconnect on the peer, my disconnect command might
1276
      # be ingored and forgotten
1277
      ever_disconnected = _IgnoreError(self._ShutdownNet, self.minor) or \
1278
                          ever_disconnected
1279
      time.sleep(sleep_time)
1280
      sleep_time = min(2, sleep_time * 1.5)
1281

    
1282
    if not status.is_standalone:
1283
      if ever_disconnected:
1284
        msg = ("drbd%d: device did not react to the"
1285
               " 'disconnect' command in a timely manner")
1286
      else:
1287
        msg = "drbd%d: can't shutdown network, even after multiple retries"
1288
      _ThrowError(msg, self.minor)
1289

    
1290
    reconfig_time = time.time() - timeout_limit + self._NET_RECONFIG_TIMEOUT
1291
    if reconfig_time > 15: # hardcoded alert limit
1292
      logging.info("drbd%d: DisconnectNet: detach took %.3f seconds",
1293
                   self.minor, reconfig_time)
1294

    
1295
  def AttachNet(self, multimaster):
1296
    """Reconnects the network.
1297

1298
    This method connects the network side of the device with a
1299
    specified multi-master flag. The device needs to be 'Standalone'
1300
    but have valid network configuration data.
1301

1302
    Args:
1303
      - multimaster: init the network in dual-primary mode
1304

1305
    """
1306
    if self.minor is None:
1307
      _ThrowError("drbd%d: device not attached in AttachNet", self._aminor)
1308

    
1309
    if None in (self._lhost, self._lport, self._rhost, self._rport):
1310
      _ThrowError("drbd%d: missing network info in AttachNet()", self.minor)
1311

    
1312
    status = self.GetProcStatus()
1313

    
1314
    if not status.is_standalone:
1315
      _ThrowError("drbd%d: device is not standalone in AttachNet", self.minor)
1316

    
1317
    self._AssembleNet(self.minor,
1318
                      (self._lhost, self._lport, self._rhost, self._rport),
1319
                      constants.DRBD_NET_PROTOCOL, dual_pri=multimaster,
1320
                      hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1321

    
1322
  def Attach(self):
1323
    """Check if our minor is configured.
1324

1325
    This doesn't do any device configurations - it only checks if the
1326
    minor is in a state different from Unconfigured.
1327

1328
    Note that this function will not change the state of the system in
1329
    any way (except in case of side-effects caused by reading from
1330
    /proc).
1331

1332
    """
1333
    used_devs = self.GetUsedDevs()
1334
    if self._aminor in used_devs:
1335
      minor = self._aminor
1336
    else:
1337
      minor = None
1338

    
1339
    self._SetFromMinor(minor)
1340
    return minor is not None
1341

    
1342
  def Assemble(self):
1343
    """Assemble the drbd.
1344

1345
    Method:
1346
      - if we have a configured device, we try to ensure that it matches
1347
        our config
1348
      - if not, we create it from zero
1349

1350
    """
1351
    super(DRBD8, self).Assemble()
1352

    
1353
    self.Attach()
1354
    if self.minor is None:
1355
      # local device completely unconfigured
1356
      self._FastAssemble()
1357
    else:
1358
      # we have to recheck the local and network status and try to fix
1359
      # the device
1360
      self._SlowAssemble()
1361

    
1362
  def _SlowAssemble(self):
1363
    """Assembles the DRBD device from a (partially) configured device.
1364

1365
    In case of partially attached (local device matches but no network
1366
    setup), we perform the network attach. If successful, we re-test
1367
    the attach if can return success.
1368

1369
    """
1370
    net_data = (self._lhost, self._lport, self._rhost, self._rport)
1371
    for minor in (self._aminor,):
1372
      info = self._GetDevInfo(self._GetShowData(minor))
1373
      match_l = self._MatchesLocal(info)
1374
      match_r = self._MatchesNet(info)
1375

    
1376
      if match_l and match_r:
1377
        # everything matches
1378
        break
1379

    
1380
      if match_l and not match_r and "local_addr" not in info:
1381
        # disk matches, but not attached to network, attach and recheck
1382
        self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
1383
                          hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1384
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1385
          break
1386
        else:
1387
          _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
1388
                      " show' disagrees", minor)
1389

    
1390
      if match_r and "local_dev" not in info:
1391
        # no local disk, but network attached and it matches
1392
        self._AssembleLocal(minor, self._children[0].dev_path,
1393
                            self._children[1].dev_path)
1394
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1395
          break
1396
        else:
1397
          _ThrowError("drbd%d: disk attach successful, but 'drbdsetup"
1398
                      " show' disagrees", minor)
1399

    
1400
      # this case must be considered only if we actually have local
1401
      # storage, i.e. not in diskless mode, because all diskless
1402
      # devices are equal from the point of view of local
1403
      # configuration
1404
      if (match_l and "local_dev" in info and
1405
          not match_r and "local_addr" in info):
1406
        # strange case - the device network part points to somewhere
1407
        # else, even though its local storage is ours; as we own the
1408
        # drbd space, we try to disconnect from the remote peer and
1409
        # reconnect to our correct one
1410
        try:
1411
          self._ShutdownNet(minor)
1412
        except errors.BlockDeviceError, err:
1413
          _ThrowError("Device has correct local storage, wrong remote peer"
1414
                      " and is unable to disconnect in order to attach to"
1415
                      " the correct peer: %s", str(err))
1416
        # note: _AssembleNet also handles the case when we don't want
1417
        # local storage (i.e. one or more of the _[lr](host|port) is
1418
        # None)
1419
        self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
1420
                          hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1421
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1422
          break
1423
        else:
1424
          _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
1425
                      " show' disagrees", minor)
1426

    
1427
    else:
1428
      minor = None
1429

    
1430
    self._SetFromMinor(minor)
1431
    if minor is None:
1432
      _ThrowError("drbd%d: cannot activate, unknown or unhandled reason",
1433
                  self._aminor)
1434

    
1435
  def _FastAssemble(self):
1436
    """Assemble the drbd device from zero.
1437

1438
    This is run when in Assemble we detect our minor is unused.
1439

1440
    """
1441
    minor = self._aminor
1442
    if self._children and self._children[0] and self._children[1]:
1443
      self._AssembleLocal(minor, self._children[0].dev_path,
1444
                          self._children[1].dev_path)
1445
    if self._lhost and self._lport and self._rhost and self._rport:
1446
      self._AssembleNet(minor,
1447
                        (self._lhost, self._lport, self._rhost, self._rport),
1448
                        constants.DRBD_NET_PROTOCOL,
1449
                        hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1450
    self._SetFromMinor(minor)
1451

    
1452
  @classmethod
1453
  def _ShutdownLocal(cls, minor):
1454
    """Detach from the local device.
1455

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

1459
    """
1460
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "detach"])
1461
    if result.failed:
1462
      _ThrowError("drbd%d: can't detach local disk: %s", minor, result.output)
1463

    
1464
  @classmethod
1465
  def _ShutdownNet(cls, minor):
1466
    """Disconnect from the remote peer.
1467

1468
    This fails if we don't have a local device.
1469

1470
    """
1471
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disconnect"])
1472
    if result.failed:
1473
      _ThrowError("drbd%d: can't shutdown network: %s", minor, result.output)
1474

    
1475
  @classmethod
1476
  def _ShutdownAll(cls, minor):
1477
    """Deactivate the device.
1478

1479
    This will, of course, fail if the device is in use.
1480

1481
    """
1482
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "down"])
1483
    if result.failed:
1484
      _ThrowError("Can't shutdown drbd device: %s", result.output)
1485

    
1486
  def Shutdown(self):
1487
    """Shutdown the DRBD device.
1488

1489
    """
1490
    if self.minor is None and not self.Attach():
1491
      logging.info("drbd%d: not attached during Shutdown()", self._aminor)
1492
      return
1493
    minor = self.minor
1494
    self.minor = None
1495
    self.dev_path = None
1496
    self._ShutdownAll(minor)
1497

    
1498
  def Remove(self):
1499
    """Stub remove for DRBD devices.
1500

1501
    """
1502
    self.Shutdown()
1503

    
1504
  @classmethod
1505
  def Create(cls, unique_id, children, size):
1506
    """Create a new DRBD8 device.
1507

1508
    Since DRBD devices are not created per se, just assembled, this
1509
    function only initializes the metadata.
1510

1511
    """
1512
    if len(children) != 2:
1513
      raise errors.ProgrammerError("Invalid setup for the drbd device")
1514
    # check that the minor is unused
1515
    aminor = unique_id[4]
1516
    proc_info = cls._MassageProcData(cls._GetProcData())
1517
    if aminor in proc_info:
1518
      status = DRBD8Status(proc_info[aminor])
1519
      in_use = status.is_in_use
1520
    else:
1521
      in_use = False
1522
    if in_use:
1523
      _ThrowError("DRBD minor %d already in use at Create() time", aminor)
1524
    meta = children[1]
1525
    meta.Assemble()
1526
    if not meta.Attach():
1527
      raise errors.BlockDeviceError("Can't attach to meta device")
1528
    if not cls._CheckMetaSize(meta.dev_path):
1529
      raise errors.BlockDeviceError("Invalid meta device size")
1530
    cls._InitMeta(aminor, meta.dev_path)
1531
    return cls(unique_id, children)
1532

    
1533
  def Grow(self, amount):
1534
    """Resize the DRBD device and its backing storage.
1535

1536
    """
1537
    if self.minor is None:
1538
      _ThrowError("drbd%d: Grow called while not attached", self._aminor)
1539
    if len(self._children) != 2 or None in self._children:
1540
      _ThrowError("drbd%d: cannot grow diskless device", self.minor)
1541
    self._children[0].Grow(amount)
1542
    result = utils.RunCmd(["drbdsetup", self.dev_path, "resize"])
1543
    if result.failed:
1544
      _ThrowError("drbd%d: resize failed: %s", self.minor, result.output)
1545

    
1546

    
1547
class FileStorage(BlockDev):
1548
  """File device.
1549

1550
  This class represents the a file storage backend device.
1551

1552
  The unique_id for the file device is a (file_driver, file_path) tuple.
1553

1554
  """
1555
  def __init__(self, unique_id, children):
1556
    """Initalizes a file device backend.
1557

1558
    """
1559
    if children:
1560
      raise errors.BlockDeviceError("Invalid setup for file device")
1561
    super(FileStorage, self).__init__(unique_id, children)
1562
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1563
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1564
    self.driver = unique_id[0]
1565
    self.dev_path = unique_id[1]
1566
    self.Attach()
1567

    
1568
  def Assemble(self):
1569
    """Assemble the device.
1570

1571
    Checks whether the file device exists, raises BlockDeviceError otherwise.
1572

1573
    """
1574
    if not os.path.exists(self.dev_path):
1575
      _ThrowError("File device '%s' does not exist" % self.dev_path)
1576

    
1577
  def Shutdown(self):
1578
    """Shutdown the device.
1579

1580
    This is a no-op for the file type, as we don't deacivate
1581
    the file on shutdown.
1582

1583
    """
1584
    pass
1585

    
1586
  def Open(self, force=False):
1587
    """Make the device ready for I/O.
1588

1589
    This is a no-op for the file type.
1590

1591
    """
1592
    pass
1593

    
1594
  def Close(self):
1595
    """Notifies that the device will no longer be used for I/O.
1596

1597
    This is a no-op for the file type.
1598

1599
    """
1600
    pass
1601

    
1602
  def Remove(self):
1603
    """Remove the file backing the block device.
1604

1605
    @rtype: boolean
1606
    @return: True if the removal was successful
1607

1608
    """
1609
    try:
1610
      os.remove(self.dev_path)
1611
    except OSError, err:
1612
      if err.errno != errno.ENOENT:
1613
        _ThrowError("Can't remove file '%s': %s", self.dev_path, err)
1614

    
1615
  def Attach(self):
1616
    """Attach to an existing file.
1617

1618
    Check if this file already exists.
1619

1620
    @rtype: boolean
1621
    @return: True if file exists
1622

1623
    """
1624
    self.attached = os.path.exists(self.dev_path)
1625
    return self.attached
1626

    
1627
  @classmethod
1628
  def Create(cls, unique_id, children, size):
1629
    """Create a new file.
1630

1631
    @param size: the size of file in MiB
1632

1633
    @rtype: L{bdev.FileStorage}
1634
    @return: an instance of FileStorage
1635

1636
    """
1637
    # TODO: decide whether we should check for existing files and
1638
    # abort or not
1639
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1640
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1641
    dev_path = unique_id[1]
1642
    try:
1643
      f = open(dev_path, 'w')
1644
      f.truncate(size * 1024 * 1024)
1645
      f.close()
1646
    except IOError, err:
1647
      _ThrowError("Error in file creation: %", str(err))
1648

    
1649
    return FileStorage(unique_id, children)
1650

    
1651

    
1652
DEV_MAP = {
1653
  constants.LD_LV: LogicalVolume,
1654
  constants.LD_DRBD8: DRBD8,
1655
  constants.LD_FILE: FileStorage,
1656
  }
1657

    
1658

    
1659
def FindDevice(dev_type, unique_id, children):
1660
  """Search for an existing, assembled device.
1661

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

1665
  """
1666
  if dev_type not in DEV_MAP:
1667
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1668
  device = DEV_MAP[dev_type](unique_id, children)
1669
  if not device.attached:
1670
    return None
1671
  return device
1672

    
1673

    
1674
def Assemble(dev_type, unique_id, children):
1675
  """Try to attach or assemble an existing device.
1676

1677
  This will attach to assemble the device, as needed, to bring it
1678
  fully up. It must be safe to run on already-assembled devices.
1679

1680
  """
1681
  if dev_type not in DEV_MAP:
1682
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1683
  device = DEV_MAP[dev_type](unique_id, children)
1684
  device.Assemble()
1685
  return device
1686

    
1687

    
1688
def Create(dev_type, unique_id, children, size):
1689
  """Create a device.
1690

1691
  """
1692
  if dev_type not in DEV_MAP:
1693
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1694
  device = DEV_MAP[dev_type].Create(unique_id, children, size)
1695
  return device