Statistics
| Branch: | Tag: | Revision:

root / lib / bdev.py @ aed77cea

History | View | Annotate | Download (52.7 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
      _ThrowError("Failed to get device size: %s - %s",
762
                  result.fail_reason, result.output)
763
    try:
764
      sectors = int(result.stdout)
765
    except ValueError:
766
      _ThrowError("Invalid output from blockdev: '%s'", result.stdout)
767
    bytes = sectors * 512
768
    if bytes < 128 * 1024 * 1024: # less than 128MiB
769
      _ThrowError("Meta device too small (%.2fMib)", (bytes / 1024 / 1024))
770
    if bytes > (128 + 32) * 1024 * 1024: # account for an extra (big) PE on LVM
771
      _ThrowError("Meta device too big (%.2fMiB)", (bytes / 1024 / 1024))
772

    
773
  def Rename(self, new_id):
774
    """Rename a device.
775

776
    This is not supported for drbd devices.
777

778
    """
779
    raise errors.ProgrammerError("Can't rename a drbd device")
780

    
781

    
782
class DRBD8(BaseDRBD):
783
  """DRBD v8.x block device.
784

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

789
  The unique_id for the drbd device is the (local_ip, local_port,
790
  remote_ip, remote_port) tuple, and it must have two children: the
791
  data device and the meta_device. The meta device is checked for
792
  valid size and is zeroed on create.
793

794
  """
795
  _MAX_MINORS = 255
796
  _PARSE_SHOW = None
797

    
798
  # timeout constants
799
  _NET_RECONFIG_TIMEOUT = 60
800

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

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

    
825
  @classmethod
826
  def _InitMeta(cls, minor, dev_path):
827
    """Initialize a meta device.
828

829
    This will not work if the given minor is in use.
830

831
    """
832
    result = utils.RunCmd(["drbdmeta", "--force", cls._DevPath(minor),
833
                           "v08", dev_path, "0", "create-md"])
834
    if result.failed:
835
      _ThrowError("Can't initialize meta device: %s", result.output)
836

    
837
  @classmethod
838
  def _FindUnusedMinor(cls):
839
    """Find an unused DRBD device.
840

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

844
    """
845
    data = cls._GetProcData()
846

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

    
865
  @classmethod
866
  def _GetShowParser(cls):
867
    """Return a parser for `drbd show` output.
868

869
    This will either create or return an already-create parser for the
870
    output of the command `drbd show`.
871

872
    """
873
    if cls._PARSE_SHOW is not None:
874
      return cls._PARSE_SHOW
875

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

    
883
    comment = pyp.Literal ("#") + pyp.Optional(pyp.restOfLine)
884
    defa = pyp.Literal("_is_default").suppress()
885
    dbl_quote = pyp.Literal('"').suppress()
886

    
887
    keyword = pyp.Word(pyp.alphanums + '-')
888

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

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

    
904
    # an entire section
905
    section_name = pyp.Word(pyp.alphas + '_')
906
    section = section_name + lbrace + pyp.ZeroOrMore(pyp.Group(stmt)) + rbrace
907

    
908
    bnf = pyp.ZeroOrMore(pyp.Group(section ^ stmt))
909
    bnf.ignore(comment)
910

    
911
    cls._PARSE_SHOW = bnf
912

    
913
    return bnf
914

    
915
  @classmethod
916
  def _GetShowData(cls, minor):
917
    """Return the `drbdsetup show` data for a minor.
918

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

    
927
  @classmethod
928
  def _GetDevInfo(cls, out):
929
    """Parse details about a given DRBD minor.
930

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

936
    """
937
    data = {}
938
    if not out:
939
      return data
940

    
941
    bnf = cls._GetShowParser()
942
    # run pyparse
943

    
944
    try:
945
      results = bnf.parseString(out)
946
    except pyp.ParseException, err:
947
      _ThrowError("Can't parse drbdsetup show output: %s", str(err))
948

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

    
967
  def _MatchesLocal(self, info):
968
    """Test if our local config matches with an existing device.
969

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

975
    """
976
    if self._children:
977
      backend, meta = self._children
978
    else:
979
      backend = meta = None
980

    
981
    if backend is not None:
982
      retval = ("local_dev" in info and info["local_dev"] == backend.dev_path)
983
    else:
984
      retval = ("local_dev" not in info)
985

    
986
    if meta is not None:
987
      retval = retval and ("meta_dev" in info and
988
                           info["meta_dev"] == meta.dev_path)
989
      retval = retval and ("meta_index" in info and
990
                           info["meta_index"] == 0)
991
    else:
992
      retval = retval and ("meta_dev" not in info and
993
                           "meta_index" not in info)
994
    return retval
995

    
996
  def _MatchesNet(self, info):
997
    """Test if our network config matches with an existing device.
998

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

1004
    """
1005
    if (((self._lhost is None and not ("local_addr" in info)) and
1006
         (self._rhost is None and not ("remote_addr" in info)))):
1007
      return True
1008

    
1009
    if self._lhost is None:
1010
      return False
1011

    
1012
    if not ("local_addr" in info and
1013
            "remote_addr" in info):
1014
      return False
1015

    
1016
    retval = (info["local_addr"] == (self._lhost, self._lport))
1017
    retval = (retval and
1018
              info["remote_addr"] == (self._rhost, self._rport))
1019
    return retval
1020

    
1021
  @classmethod
1022
  def _AssembleLocal(cls, minor, backend, meta):
1023
    """Configure the local part of a DRBD device.
1024

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

    
1032
  @classmethod
1033
  def _AssembleNet(cls, minor, net_info, protocol,
1034
                   dual_pri=False, hmac=None, secret=None):
1035
    """Configure the network part of the device.
1036

1037
    """
1038
    lhost, lport, rhost, rport = net_info
1039
    if None in net_info:
1040
      # we don't want network connection and actually want to make
1041
      # sure its shutdown
1042
      cls._ShutdownNet(minor)
1043
      return
1044

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

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

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

    
1084
  def AddChildren(self, devices):
1085
    """Add a disk to the DRBD device.
1086

1087
    """
1088
    if self.minor is None:
1089
      _ThrowError("drbd%d: can't attach to dbrd8 during AddChildren",
1090
                  self._aminor)
1091
    if len(devices) != 2:
1092
      _ThrowError("drbd%d: need two devices for AddChildren", self.minor)
1093
    info = self._GetDevInfo(self._GetShowData(self.minor))
1094
    if "local_dev" in info:
1095
      _ThrowError("drbd%d: already attached to a local disk", self.minor)
1096
    backend, meta = devices
1097
    if backend.dev_path is None or meta.dev_path is None:
1098
      _ThrowError("drbd%d: children not ready during AddChildren", self.minor)
1099
    backend.Open()
1100
    meta.Open()
1101
    self._CheckMetaSize(meta.dev_path)
1102
    self._InitMeta(self._FindUnusedMinor(), meta.dev_path)
1103

    
1104
    self._AssembleLocal(self.minor, backend.dev_path, meta.dev_path)
1105
    self._children = devices
1106

    
1107
  def RemoveChildren(self, devices):
1108
    """Detach the drbd device from local storage.
1109

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

    
1131
    self._ShutdownLocal(self.minor)
1132
    self._children = []
1133

    
1134
  @classmethod
1135
  def _SetMinorSyncSpeed(cls, minor, kbytes):
1136
    """Set the speed of the DRBD syncer.
1137

1138
    This is the low-level implementation.
1139

1140
    @type minor: int
1141
    @param minor: the drbd minor whose settings we change
1142
    @type kbytes: int
1143
    @param kbytes: the speed in kbytes/second
1144
    @rtype: boolean
1145
    @return: the success of the operation
1146

1147
    """
1148
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "syncer",
1149
                           "-r", "%d" % kbytes, "--create-device"])
1150
    if result.failed:
1151
      logging.error("Can't change syncer rate: %s - %s",
1152
                    result.fail_reason, result.output)
1153
    return not result.failed
1154

    
1155
  def SetSyncSpeed(self, kbytes):
1156
    """Set the speed of the DRBD syncer.
1157

1158
    @type kbytes: int
1159
    @param kbytes: the speed in kbytes/second
1160
    @rtype: boolean
1161
    @return: the success of the operation
1162

1163
    """
1164
    if self.minor is None:
1165
      logging.info("Not attached during SetSyncSpeed")
1166
      return False
1167
    children_result = super(DRBD8, self).SetSyncSpeed(kbytes)
1168
    return self._SetMinorSyncSpeed(self.minor, kbytes) and children_result
1169

    
1170
  def GetProcStatus(self):
1171
    """Return device data from /proc.
1172

1173
    """
1174
    if self.minor is None:
1175
      _ThrowError("drbd%d: GetStats() called while not attached", self._aminor)
1176
    proc_info = self._MassageProcData(self._GetProcData())
1177
    if self.minor not in proc_info:
1178
      _ThrowError("drbd%d: can't find myself in /proc", self.minor)
1179
    return DRBD8Status(proc_info[self.minor])
1180

    
1181
  def GetSyncStatus(self):
1182
    """Returns the sync status of the device.
1183

1184

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

1189

1190
    We set the is_degraded parameter to True on two conditions:
1191
    network not connected or local disk missing.
1192

1193
    We compute the ldisk parameter based on wheter we have a local
1194
    disk or not.
1195

1196
    @rtype: tuple
1197
    @return: (sync_percent, estimated_time, is_degraded, ldisk)
1198

1199
    """
1200
    if self.minor is None and not self.Attach():
1201
      _ThrowError("drbd%d: can't Attach() in GetSyncStatus", self._aminor)
1202
    stats = self.GetProcStatus()
1203
    ldisk = not stats.is_disk_uptodate
1204
    is_degraded = not stats.is_connected
1205
    return stats.sync_percent, stats.est_time, is_degraded or ldisk, ldisk
1206

    
1207
  def Open(self, force=False):
1208
    """Make the local state primary.
1209

1210
    If the 'force' parameter is given, the '-o' option is passed to
1211
    drbdsetup. Since this is a potentially dangerous operation, the
1212
    force flag should be only given after creation, when it actually
1213
    is mandatory.
1214

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

    
1227
  def Close(self):
1228
    """Make the local state secondary.
1229

1230
    This will, of course, fail if the device is in use.
1231

1232
    """
1233
    if self.minor is None and not self.Attach():
1234
      _ThrowError("drbd%d: can't Attach() in Close()", self._aminor)
1235
    result = utils.RunCmd(["drbdsetup", self.dev_path, "secondary"])
1236
    if result.failed:
1237
      _ThrowError("drbd%d: can't switch drbd device to secondary: %s",
1238
                  self.minor, result.output)
1239

    
1240
  def DisconnectNet(self):
1241
    """Removes network configuration.
1242

1243
    This method shutdowns the network side of the device.
1244

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

1253
    """
1254
    if self.minor is None:
1255
      _ThrowError("drbd%d: disk not attached in re-attach net", self._aminor)
1256

    
1257
    if None in (self._lhost, self._lport, self._rhost, self._rport):
1258
      _ThrowError("drbd%d: DRBD disk missing network info in"
1259
                  " DisconnectNet()", self.minor)
1260

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

    
1276
    if not status.is_standalone:
1277
      if ever_disconnected:
1278
        msg = ("drbd%d: device did not react to the"
1279
               " 'disconnect' command in a timely manner")
1280
      else:
1281
        msg = "drbd%d: can't shutdown network, even after multiple retries"
1282
      _ThrowError(msg, self.minor)
1283

    
1284
    reconfig_time = time.time() - timeout_limit + self._NET_RECONFIG_TIMEOUT
1285
    if reconfig_time > 15: # hardcoded alert limit
1286
      logging.info("drbd%d: DisconnectNet: detach took %.3f seconds",
1287
                   self.minor, reconfig_time)
1288

    
1289
  def AttachNet(self, multimaster):
1290
    """Reconnects the network.
1291

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

1296
    Args:
1297
      - multimaster: init the network in dual-primary mode
1298

1299
    """
1300
    if self.minor is None:
1301
      _ThrowError("drbd%d: device not attached in AttachNet", self._aminor)
1302

    
1303
    if None in (self._lhost, self._lport, self._rhost, self._rport):
1304
      _ThrowError("drbd%d: missing network info in AttachNet()", self.minor)
1305

    
1306
    status = self.GetProcStatus()
1307

    
1308
    if not status.is_standalone:
1309
      _ThrowError("drbd%d: device is not standalone in AttachNet", self.minor)
1310

    
1311
    self._AssembleNet(self.minor,
1312
                      (self._lhost, self._lport, self._rhost, self._rport),
1313
                      constants.DRBD_NET_PROTOCOL, dual_pri=multimaster,
1314
                      hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1315

    
1316
  def Attach(self):
1317
    """Check if our minor is configured.
1318

1319
    This doesn't do any device configurations - it only checks if the
1320
    minor is in a state different from Unconfigured.
1321

1322
    Note that this function will not change the state of the system in
1323
    any way (except in case of side-effects caused by reading from
1324
    /proc).
1325

1326
    """
1327
    used_devs = self.GetUsedDevs()
1328
    if self._aminor in used_devs:
1329
      minor = self._aminor
1330
    else:
1331
      minor = None
1332

    
1333
    self._SetFromMinor(minor)
1334
    return minor is not None
1335

    
1336
  def Assemble(self):
1337
    """Assemble the drbd.
1338

1339
    Method:
1340
      - if we have a configured device, we try to ensure that it matches
1341
        our config
1342
      - if not, we create it from zero
1343

1344
    """
1345
    super(DRBD8, self).Assemble()
1346

    
1347
    self.Attach()
1348
    if self.minor is None:
1349
      # local device completely unconfigured
1350
      self._FastAssemble()
1351
    else:
1352
      # we have to recheck the local and network status and try to fix
1353
      # the device
1354
      self._SlowAssemble()
1355

    
1356
  def _SlowAssemble(self):
1357
    """Assembles the DRBD device from a (partially) configured device.
1358

1359
    In case of partially attached (local device matches but no network
1360
    setup), we perform the network attach. If successful, we re-test
1361
    the attach if can return success.
1362

1363
    """
1364
    net_data = (self._lhost, self._lport, self._rhost, self._rport)
1365
    for minor in (self._aminor,):
1366
      info = self._GetDevInfo(self._GetShowData(minor))
1367
      match_l = self._MatchesLocal(info)
1368
      match_r = self._MatchesNet(info)
1369

    
1370
      if match_l and match_r:
1371
        # everything matches
1372
        break
1373

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

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

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

    
1421
    else:
1422
      minor = None
1423

    
1424
    self._SetFromMinor(minor)
1425
    if minor is None:
1426
      _ThrowError("drbd%d: cannot activate, unknown or unhandled reason",
1427
                  self._aminor)
1428

    
1429
  def _FastAssemble(self):
1430
    """Assemble the drbd device from zero.
1431

1432
    This is run when in Assemble we detect our minor is unused.
1433

1434
    """
1435
    minor = self._aminor
1436
    if self._children and self._children[0] and self._children[1]:
1437
      self._AssembleLocal(minor, self._children[0].dev_path,
1438
                          self._children[1].dev_path)
1439
    if self._lhost and self._lport and self._rhost and self._rport:
1440
      self._AssembleNet(minor,
1441
                        (self._lhost, self._lport, self._rhost, self._rport),
1442
                        constants.DRBD_NET_PROTOCOL,
1443
                        hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1444
    self._SetFromMinor(minor)
1445

    
1446
  @classmethod
1447
  def _ShutdownLocal(cls, minor):
1448
    """Detach from the local device.
1449

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

1453
    """
1454
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "detach"])
1455
    if result.failed:
1456
      _ThrowError("drbd%d: can't detach local disk: %s", minor, result.output)
1457

    
1458
  @classmethod
1459
  def _ShutdownNet(cls, minor):
1460
    """Disconnect from the remote peer.
1461

1462
    This fails if we don't have a local device.
1463

1464
    """
1465
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disconnect"])
1466
    if result.failed:
1467
      _ThrowError("drbd%d: can't shutdown network: %s", minor, result.output)
1468

    
1469
  @classmethod
1470
  def _ShutdownAll(cls, minor):
1471
    """Deactivate the device.
1472

1473
    This will, of course, fail if the device is in use.
1474

1475
    """
1476
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "down"])
1477
    if result.failed:
1478
      _ThrowError("drbd%d: can't shutdown drbd device: %s",
1479
                  minor, result.output)
1480

    
1481
  def Shutdown(self):
1482
    """Shutdown the DRBD device.
1483

1484
    """
1485
    if self.minor is None and not self.Attach():
1486
      logging.info("drbd%d: not attached during Shutdown()", self._aminor)
1487
      return
1488
    minor = self.minor
1489
    self.minor = None
1490
    self.dev_path = None
1491
    self._ShutdownAll(minor)
1492

    
1493
  def Remove(self):
1494
    """Stub remove for DRBD devices.
1495

1496
    """
1497
    self.Shutdown()
1498

    
1499
  @classmethod
1500
  def Create(cls, unique_id, children, size):
1501
    """Create a new DRBD8 device.
1502

1503
    Since DRBD devices are not created per se, just assembled, this
1504
    function only initializes the metadata.
1505

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

    
1528
  def Grow(self, amount):
1529
    """Resize the DRBD device and its backing storage.
1530

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

    
1541

    
1542
class FileStorage(BlockDev):
1543
  """File device.
1544

1545
  This class represents the a file storage backend device.
1546

1547
  The unique_id for the file device is a (file_driver, file_path) tuple.
1548

1549
  """
1550
  def __init__(self, unique_id, children):
1551
    """Initalizes a file device backend.
1552

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

    
1563
  def Assemble(self):
1564
    """Assemble the device.
1565

1566
    Checks whether the file device exists, raises BlockDeviceError otherwise.
1567

1568
    """
1569
    if not os.path.exists(self.dev_path):
1570
      _ThrowError("File device '%s' does not exist" % self.dev_path)
1571

    
1572
  def Shutdown(self):
1573
    """Shutdown the device.
1574

1575
    This is a no-op for the file type, as we don't deacivate
1576
    the file on shutdown.
1577

1578
    """
1579
    pass
1580

    
1581
  def Open(self, force=False):
1582
    """Make the device ready for I/O.
1583

1584
    This is a no-op for the file type.
1585

1586
    """
1587
    pass
1588

    
1589
  def Close(self):
1590
    """Notifies that the device will no longer be used for I/O.
1591

1592
    This is a no-op for the file type.
1593

1594
    """
1595
    pass
1596

    
1597
  def Remove(self):
1598
    """Remove the file backing the block device.
1599

1600
    @rtype: boolean
1601
    @return: True if the removal was successful
1602

1603
    """
1604
    try:
1605
      os.remove(self.dev_path)
1606
    except OSError, err:
1607
      if err.errno != errno.ENOENT:
1608
        _ThrowError("Can't remove file '%s': %s", self.dev_path, err)
1609

    
1610
  def Attach(self):
1611
    """Attach to an existing file.
1612

1613
    Check if this file already exists.
1614

1615
    @rtype: boolean
1616
    @return: True if file exists
1617

1618
    """
1619
    self.attached = os.path.exists(self.dev_path)
1620
    return self.attached
1621

    
1622
  @classmethod
1623
  def Create(cls, unique_id, children, size):
1624
    """Create a new file.
1625

1626
    @param size: the size of file in MiB
1627

1628
    @rtype: L{bdev.FileStorage}
1629
    @return: an instance of FileStorage
1630

1631
    """
1632
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1633
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1634
    dev_path = unique_id[1]
1635
    if os.path.exists(dev_path):
1636
      _ThrowError("File already existing: %s", dev_path)
1637
    try:
1638
      f = open(dev_path, 'w')
1639
      f.truncate(size * 1024 * 1024)
1640
      f.close()
1641
    except IOError, err:
1642
      _ThrowError("Error in file creation: %", str(err))
1643

    
1644
    return FileStorage(unique_id, children)
1645

    
1646

    
1647
DEV_MAP = {
1648
  constants.LD_LV: LogicalVolume,
1649
  constants.LD_DRBD8: DRBD8,
1650
  constants.LD_FILE: FileStorage,
1651
  }
1652

    
1653

    
1654
def FindDevice(dev_type, unique_id, children):
1655
  """Search for an existing, assembled device.
1656

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

1660
  """
1661
  if dev_type not in DEV_MAP:
1662
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1663
  device = DEV_MAP[dev_type](unique_id, children)
1664
  if not device.attached:
1665
    return None
1666
  return device
1667

    
1668

    
1669
def Assemble(dev_type, unique_id, children):
1670
  """Try to attach or assemble an existing device.
1671

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

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

    
1682

    
1683
def Create(dev_type, unique_id, children, size):
1684
  """Create a device.
1685

1686
  """
1687
  if dev_type not in DEV_MAP:
1688
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1689
  device = DEV_MAP[dev_type].Create(unique_id, children, size)
1690
  return device