Statistics
| Branch: | Tag: | Revision:

root / lib / bdev.py @ 6d2e83d5

History | View | Annotate | Download (55 kB)

1
#
2
#
3

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

    
21

    
22
"""Block device abstraction"""
23

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

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

    
35

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

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

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

51
  The many different states of the device are due to the fact that we
52
  need to cover many device types:
53
    - logical volumes are created, lvchange -a y $lv, and used
54
    - drbd devices are attached to a local disk/remote peer and made primary
55

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

61
  Not all devices implement both the first two as distinct items. LVM
62
  logical volumes have their unique ID (the pair volume group, logical
63
  volume name) in a 1-to-1 relation to the dev path. For DRBD devices,
64
  the /dev path is again dynamic and the unique id is the pair (host1,
65
  dev1), (host2, dev2).
66

67
  You can get to a device in two ways:
68
    - creating the (real) device, which returns you
69
      an attached instance (lvcreate)
70
    - attaching of a python instance to an existing (real) device
71

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

78
  """
79
  def __init__(self, unique_id, children):
80
    self._children = children
81
    self.dev_path = None
82
    self.unique_id = unique_id
83
    self.major = None
84
    self.minor = None
85
    self.attached = False
86

    
87
  def Assemble(self):
88
    """Assemble the device from its components.
89

90
    Implementations of this method by child classes must ensure that:
91
      - after the device has been assembled, it knows its major/minor
92
        numbers; this allows other devices (usually parents) to probe
93
        correctly for their children
94
      - calling this method on an existing, in-use device is safe
95
      - if the device is already configured (and in an OK state),
96
        this method is idempotent
97

98
    """
99
    return True
100

    
101
  def Attach(self):
102
    """Find a device which matches our config and attach to it.
103

104
    """
105
    raise NotImplementedError
106

    
107
  def Close(self):
108
    """Notifies that the device will no longer be used for I/O.
109

110
    """
111
    raise NotImplementedError
112

    
113
  @classmethod
114
  def Create(cls, unique_id, children, size):
115
    """Create the device.
116

117
    If the device cannot be created, it will return None
118
    instead. Error messages go to the logging system.
119

120
    Note that for some devices, the unique_id is used, and for other,
121
    the children. The idea is that these two, taken together, are
122
    enough for both creation and assembly (later).
123

124
    """
125
    raise NotImplementedError
126

    
127
  def Remove(self):
128
    """Remove this device.
129

130
    This makes sense only for some of the device types: LV and file
131
    storeage. Also note that if the device can't attach, the removal
132
    can't be completed.
133

134
    """
135
    raise NotImplementedError
136

    
137
  def Rename(self, new_id):
138
    """Rename this device.
139

140
    This may or may not make sense for a given device type.
141

142
    """
143
    raise NotImplementedError
144

    
145
  def Open(self, force=False):
146
    """Make the device ready for use.
147

148
    This makes the device ready for I/O. For now, just the DRBD
149
    devices need this.
150

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

154
    """
155
    raise NotImplementedError
156

    
157
  def Shutdown(self):
158
    """Shut down the device, freeing its children.
159

160
    This undoes the `Assemble()` work, except for the child
161
    assembling; as such, the children on the device are still
162
    assembled after this call.
163

164
    """
165
    raise NotImplementedError
166

    
167
  def SetSyncSpeed(self, speed):
168
    """Adjust the sync speed of the mirror.
169

170
    In case this is not a mirroring device, this is no-op.
171

172
    """
173
    result = True
174
    if self._children:
175
      for child in self._children:
176
        result = result and child.SetSyncSpeed(speed)
177
    return result
178

    
179
  def GetSyncStatus(self):
180
    """Returns the sync status of the device.
181

182
    If this device is a mirroring device, this function returns the
183
    status of the mirror.
184

185
    If sync_percent is None, it means the device is not syncing.
186

187
    If estimated_time is None, it means we can't estimate
188
    the time needed, otherwise it's the time left in seconds.
189

190
    If is_degraded is True, it means the device is missing
191
    redundancy. This is usually a sign that something went wrong in
192
    the device setup, if sync_percent is None.
193

194
    The ldisk parameter represents the degradation of the local
195
    data. This is only valid for some devices, the rest will always
196
    return False (not degraded).
197

198
    @rtype: tuple
199
    @return: (sync_percent, estimated_time, is_degraded, ldisk)
200

201
    """
202
    return None, None, False, False
203

    
204

    
205
  def CombinedSyncStatus(self):
206
    """Calculate the mirror status recursively for our children.
207

208
    The return value is the same as for `GetSyncStatus()` except the
209
    minimum percent and maximum time are calculated across our
210
    children.
211

212
    """
213
    min_percent, max_time, is_degraded, ldisk = self.GetSyncStatus()
214
    if self._children:
215
      for child in self._children:
216
        c_percent, c_time, c_degraded, c_ldisk = child.GetSyncStatus()
217
        if min_percent is None:
218
          min_percent = c_percent
219
        elif c_percent is not None:
220
          min_percent = min(min_percent, c_percent)
221
        if max_time is None:
222
          max_time = c_time
223
        elif c_time is not None:
224
          max_time = max(max_time, c_time)
225
        is_degraded = is_degraded or c_degraded
226
        ldisk = ldisk or c_ldisk
227
    return min_percent, max_time, is_degraded, ldisk
228

    
229

    
230
  def SetInfo(self, text):
231
    """Update metadata with info text.
232

233
    Only supported for some device types.
234

235
    """
236
    for child in self._children:
237
      child.SetInfo(text)
238

    
239
  def Grow(self, amount):
240
    """Grow the block device.
241

242
    @param amount: the amount (in mebibytes) to grow with
243

244
    """
245
    raise NotImplementedError
246

    
247
  def __repr__(self):
248
    return ("<%s: unique_id: %s, children: %s, %s:%s, %s>" %
249
            (self.__class__, self.unique_id, self._children,
250
             self.major, self.minor, self.dev_path))
251

    
252

    
253
class LogicalVolume(BlockDev):
254
  """Logical Volume block device.
255

256
  """
257
  def __init__(self, unique_id, children):
258
    """Attaches to a LV device.
259

260
    The unique_id is a tuple (vg_name, lv_name)
261

262
    """
263
    super(LogicalVolume, self).__init__(unique_id, children)
264
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
265
      raise ValueError("Invalid configuration data %s" % str(unique_id))
266
    self._vg_name, self._lv_name = unique_id
267
    self.dev_path = "/dev/%s/%s" % (self._vg_name, self._lv_name)
268
    self._degraded = True
269
    self.major = self.minor = None
270
    self.Attach()
271

    
272
  @classmethod
273
  def Create(cls, unique_id, children, size):
274
    """Create a new logical volume.
275

276
    """
277
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
278
      raise errors.ProgrammerError("Invalid configuration data %s" %
279
                                   str(unique_id))
280
    vg_name, lv_name = unique_id
281
    pvs_info = cls.GetPVInfo(vg_name)
282
    if not pvs_info:
283
      raise errors.BlockDeviceError("Can't compute PV info for vg %s" %
284
                                    vg_name)
285
    pvs_info.sort()
286
    pvs_info.reverse()
287

    
288
    pvlist = [ pv[1] for pv in pvs_info ]
289
    free_size = sum([ pv[0] for pv in pvs_info ])
290

    
291
    # The size constraint should have been checked from the master before
292
    # calling the create function.
293
    if free_size < size:
294
      raise errors.BlockDeviceError("Not enough free space: required %s,"
295
                                    " available %s" % (size, free_size))
296
    result = utils.RunCmd(["lvcreate", "-L%dm" % size, "-n%s" % lv_name,
297
                           vg_name] + pvlist)
298
    if result.failed:
299
      raise errors.BlockDeviceError("LV create failed (%s): %s" %
300
                                    (result.fail_reason, result.output))
301
    return LogicalVolume(unique_id, children)
302

    
303
  @staticmethod
304
  def GetPVInfo(vg_name):
305
    """Get the free space info for PVs in a volume group.
306

307
    @param vg_name: the volume group name
308

309
    @rtype: list
310
    @return: list of tuples (free_space, name) with free_space in mebibytes
311

312
    """
313
    command = ["pvs", "--noheadings", "--nosuffix", "--units=m",
314
               "-opv_name,vg_name,pv_free,pv_attr", "--unbuffered",
315
               "--separator=:"]
316
    result = utils.RunCmd(command)
317
    if result.failed:
318
      logging.error("Can't get the PV information: %s - %s",
319
                    result.fail_reason, result.output)
320
      return None
321
    data = []
322
    for line in result.stdout.splitlines():
323
      fields = line.strip().split(':')
324
      if len(fields) != 4:
325
        logging.error("Can't parse pvs output: line '%s'", line)
326
        return None
327
      # skip over pvs from another vg or ones which are not allocatable
328
      if fields[1] != vg_name or fields[3][0] != 'a':
329
        continue
330
      data.append((float(fields[2]), fields[0]))
331

    
332
    return data
333

    
334
  def Remove(self):
335
    """Remove this logical volume.
336

337
    """
338
    if not self.minor and not self.Attach():
339
      # the LV does not exist
340
      return True
341
    result = utils.RunCmd(["lvremove", "-f", "%s/%s" %
342
                           (self._vg_name, self._lv_name)])
343
    if result.failed:
344
      logging.error("Can't lvremove: %s - %s",
345
                    result.fail_reason, result.output)
346

    
347
    return not result.failed
348

    
349
  def Rename(self, new_id):
350
    """Rename this logical volume.
351

352
    """
353
    if not isinstance(new_id, (tuple, list)) or len(new_id) != 2:
354
      raise errors.ProgrammerError("Invalid new logical id '%s'" % new_id)
355
    new_vg, new_name = new_id
356
    if new_vg != self._vg_name:
357
      raise errors.ProgrammerError("Can't move a logical volume across"
358
                                   " volume groups (from %s to to %s)" %
359
                                   (self._vg_name, new_vg))
360
    result = utils.RunCmd(["lvrename", new_vg, self._lv_name, new_name])
361
    if result.failed:
362
      raise errors.BlockDeviceError("Failed to rename the logical volume: %s" %
363
                                    result.output)
364
    self._lv_name = new_name
365
    self.dev_path = "/dev/%s/%s" % (self._vg_name, self._lv_name)
366

    
367
  def Attach(self):
368
    """Attach to an existing LV.
369

370
    This method will try to see if an existing and active LV exists
371
    which matches our name. If so, its major/minor will be
372
    recorded.
373

374
    """
375
    self.attached = False
376
    result = utils.RunCmd(["lvs", "--noheadings", "--separator=,",
377
                           "-olv_attr,lv_kernel_major,lv_kernel_minor",
378
                           self.dev_path])
379
    if result.failed:
380
      logging.error("Can't find LV %s: %s, %s",
381
                    self.dev_path, result.fail_reason, result.output)
382
      return False
383
    out = result.stdout.strip().rstrip(',')
384
    out = out.split(",")
385
    if len(out) != 3:
386
      logging.error("Can't parse LVS output, len(%s) != 3", str(out))
387
      return False
388

    
389
    status, major, minor = out[:3]
390
    if len(status) != 6:
391
      logging.error("lvs lv_attr is not 6 characters (%s)", status)
392
      return False
393

    
394
    try:
395
      major = int(major)
396
      minor = int(minor)
397
    except ValueError, err:
398
      logging.error("lvs major/minor cannot be parsed: %s", str(err))
399

    
400
    self.major = major
401
    self.minor = minor
402
    self._degraded = status[0] == 'v' # virtual volume, i.e. doesn't backing
403
                                      # storage
404
    self.attached = True
405
    return True
406

    
407
  def Assemble(self):
408
    """Assemble the device.
409

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

414
    """
415
    result = utils.RunCmd(["lvchange", "-ay", self.dev_path])
416
    if result.failed:
417
      logging.error("Can't activate lv %s: %s", self.dev_path, result.output)
418
      return False
419
    return self.Attach()
420

    
421
  def Shutdown(self):
422
    """Shutdown the device.
423

424
    This is a no-op for the LV device type, as we don't deactivate the
425
    volumes on shutdown.
426

427
    """
428
    return True
429

    
430
  def GetSyncStatus(self):
431
    """Returns the sync status of the device.
432

433
    If this device is a mirroring device, this function returns the
434
    status of the mirror.
435

436
    For logical volumes, sync_percent and estimated_time are always
437
    None (no recovery in progress, as we don't handle the mirrored LV
438
    case). The is_degraded parameter is the inverse of the ldisk
439
    parameter.
440

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

447
    The status was already read in Attach, so we just return it.
448

449
    @rtype: tuple
450
    @return: (sync_percent, estimated_time, is_degraded, ldisk)
451

452
    """
453
    return None, None, self._degraded, self._degraded
454

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

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

460
    """
461
    pass
462

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

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

468
    """
469
    pass
470

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

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

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

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

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

    
499
    return snap_name
500

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

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

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

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

    
514
    result = utils.RunCmd(["lvchange", "--addtag", text,
515
                           self.dev_path])
516
    if result.failed:
517
      raise errors.BlockDeviceError("Command: %s error: %s - %s" %
518
                                    (result.cmd, result.fail_reason,
519
                                     result.output))
520
  def Grow(self, amount):
521
    """Grow the logical volume.
522

523
    """
524
    # we try multiple algorithms since the 'best' ones might not have
525
    # space available in the right place, but later ones might (since
526
    # they have less constraints); also note that only recent LVM
527
    # supports 'cling'
528
    for alloc_policy in "contiguous", "cling", "normal":
529
      result = utils.RunCmd(["lvextend", "--alloc", alloc_policy,
530
                             "-L", "+%dm" % amount, self.dev_path])
531
      if not result.failed:
532
        return
533
    raise errors.BlockDeviceError("Can't grow LV %s: %s" %
534
                                  (self.dev_path, result.output))
535

    
536

    
537
class DRBD8Status(object):
538
  """A DRBD status representation class.
539

540
  Note that this doesn't support unconfigured devices (cs:Unconfigured).
541

542
  """
543
  UNCONF_RE = re.compile(r"\s*[0-9]+:\s*cs:Unconfigured$")
544
  LINE_RE = re.compile(r"\s*[0-9]+:\s*cs:(\S+)\s+st:([^/]+)/(\S+)"
545
                       "\s+ds:([^/]+)/(\S+)\s+.*$")
546
  SYNC_RE = re.compile(r"^.*\ssync'ed:\s*([0-9.]+)%.*"
547
                       "\sfinish: ([0-9]+):([0-9]+):([0-9]+)\s.*$")
548

    
549
  def __init__(self, procline):
550
    u = self.UNCONF_RE.match(procline)
551
    if u:
552
      self.cstatus = "Unconfigured"
553
      self.lrole = self.rrole = self.ldisk = self.rdisk = None
554
    else:
555
      m = self.LINE_RE.match(procline)
556
      if not m:
557
        raise errors.BlockDeviceError("Can't parse input data '%s'" % procline)
558
      self.cstatus = m.group(1)
559
      self.lrole = m.group(2)
560
      self.rrole = m.group(3)
561
      self.ldisk = m.group(4)
562
      self.rdisk = m.group(5)
563

    
564
    # end reading of data from the LINE_RE or UNCONF_RE
565

    
566
    self.is_standalone = self.cstatus == "StandAlone"
567
    self.is_wfconn = self.cstatus == "WFConnection"
568
    self.is_connected = self.cstatus == "Connected"
569
    self.is_primary = self.lrole == "Primary"
570
    self.is_secondary = self.lrole == "Secondary"
571
    self.peer_primary = self.rrole == "Primary"
572
    self.peer_secondary = self.rrole == "Secondary"
573
    self.both_primary = self.is_primary and self.peer_primary
574
    self.both_secondary = self.is_secondary and self.peer_secondary
575

    
576
    self.is_diskless = self.ldisk == "Diskless"
577
    self.is_disk_uptodate = self.ldisk == "UpToDate"
578

    
579
    self.is_in_resync = self.cstatus in ("SyncSource", "SyncTarget")
580
    self.is_in_use = self.cstatus != "Unconfigured"
581

    
582
    m = self.SYNC_RE.match(procline)
583
    if m:
584
      self.sync_percent = float(m.group(1))
585
      hours = int(m.group(2))
586
      minutes = int(m.group(3))
587
      seconds = int(m.group(4))
588
      self.est_time = hours * 3600 + minutes * 60 + seconds
589
    else:
590
      self.sync_percent = None
591
      self.est_time = None
592

    
593
    self.is_sync_target = self.peer_sync_source = self.cstatus == "SyncTarget"
594
    self.peer_sync_target = self.is_sync_source = self.cstatus == "SyncSource"
595
    self.is_resync = self.is_sync_target or self.is_sync_source
596

    
597

    
598
class BaseDRBD(BlockDev):
599
  """Base DRBD class.
600

601
  This class contains a few bits of common functionality between the
602
  0.7 and 8.x versions of DRBD.
603

604
  """
605
  _VERSION_RE = re.compile(r"^version: (\d+)\.(\d+)\.(\d+)"
606
                           r" \(api:(\d+)/proto:(\d+)(?:-(\d+))?\)")
607

    
608
  _DRBD_MAJOR = 147
609
  _ST_UNCONFIGURED = "Unconfigured"
610
  _ST_WFCONNECTION = "WFConnection"
611
  _ST_CONNECTED = "Connected"
612

    
613
  _STATUS_FILE = "/proc/drbd"
614

    
615
  @staticmethod
616
  def _GetProcData(filename=_STATUS_FILE):
617
    """Return data from /proc/drbd.
618

619
    """
620
    stat = open(filename, "r")
621
    try:
622
      data = stat.read().splitlines()
623
    finally:
624
      stat.close()
625
    if not data:
626
      raise errors.BlockDeviceError("Can't read any data from %s" % filename)
627
    return data
628

    
629
  @staticmethod
630
  def _MassageProcData(data):
631
    """Transform the output of _GetProdData into a nicer form.
632

633
    @return: a dictionary of minor: joined lines from /proc/drbd
634
        for that minor
635

636
    """
637
    lmatch = re.compile("^ *([0-9]+):.*$")
638
    results = {}
639
    old_minor = old_line = None
640
    for line in data:
641
      lresult = lmatch.match(line)
642
      if lresult is not None:
643
        if old_minor is not None:
644
          results[old_minor] = old_line
645
        old_minor = int(lresult.group(1))
646
        old_line = line
647
      else:
648
        if old_minor is not None:
649
          old_line += " " + line.strip()
650
    # add last line
651
    if old_minor is not None:
652
      results[old_minor] = old_line
653
    return results
654

    
655
  @classmethod
656
  def _GetVersion(cls):
657
    """Return the DRBD version.
658

659
    This will return a dict with keys:
660
      - k_major
661
      - k_minor
662
      - k_point
663
      - api
664
      - proto
665
      - proto2 (only on drbd > 8.2.X)
666

667
    """
668
    proc_data = cls._GetProcData()
669
    first_line = proc_data[0].strip()
670
    version = cls._VERSION_RE.match(first_line)
671
    if not version:
672
      raise errors.BlockDeviceError("Can't parse DRBD version from '%s'" %
673
                                    first_line)
674

    
675
    values = version.groups()
676
    retval = {'k_major': int(values[0]),
677
              'k_minor': int(values[1]),
678
              'k_point': int(values[2]),
679
              'api': int(values[3]),
680
              'proto': int(values[4]),
681
             }
682
    if values[5] is not None:
683
      retval['proto2'] = values[5]
684

    
685
    return retval
686

    
687
  @staticmethod
688
  def _DevPath(minor):
689
    """Return the path to a drbd device for a given minor.
690

691
    """
692
    return "/dev/drbd%d" % minor
693

    
694
  @classmethod
695
  def GetUsedDevs(cls):
696
    """Compute the list of used DRBD devices.
697

698
    """
699
    data = cls._GetProcData()
700

    
701
    used_devs = {}
702
    valid_line = re.compile("^ *([0-9]+): cs:([^ ]+).*$")
703
    for line in data:
704
      match = valid_line.match(line)
705
      if not match:
706
        continue
707
      minor = int(match.group(1))
708
      state = match.group(2)
709
      if state == cls._ST_UNCONFIGURED:
710
        continue
711
      used_devs[minor] = state, line
712

    
713
    return used_devs
714

    
715
  def _SetFromMinor(self, minor):
716
    """Set our parameters based on the given minor.
717

718
    This sets our minor variable and our dev_path.
719

720
    """
721
    if minor is None:
722
      self.minor = self.dev_path = None
723
      self.attached = False
724
    else:
725
      self.minor = minor
726
      self.dev_path = self._DevPath(minor)
727
      self.attached = True
728

    
729
  @staticmethod
730
  def _CheckMetaSize(meta_device):
731
    """Check if the given meta device looks like a valid one.
732

733
    This currently only check the size, which must be around
734
    128MiB.
735

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

    
756
  def Rename(self, new_id):
757
    """Rename a device.
758

759
    This is not supported for drbd devices.
760

761
    """
762
    raise errors.ProgrammerError("Can't rename a drbd device")
763

    
764

    
765
class DRBD8(BaseDRBD):
766
  """DRBD v8.x block device.
767

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

772
  The unique_id for the drbd device is the (local_ip, local_port,
773
  remote_ip, remote_port) tuple, and it must have two children: the
774
  data device and the meta_device. The meta device is checked for
775
  valid size and is zeroed on create.
776

777
  """
778
  _MAX_MINORS = 255
779
  _PARSE_SHOW = None
780

    
781
  # timeout constants
782
  _NET_RECONFIG_TIMEOUT = 60
783

    
784
  def __init__(self, unique_id, children):
785
    if children and children.count(None) > 0:
786
      children = []
787
    super(DRBD8, self).__init__(unique_id, children)
788
    self.major = self._DRBD_MAJOR
789
    version = self._GetVersion()
790
    if version['k_major'] != 8 :
791
      raise errors.BlockDeviceError("Mismatch in DRBD kernel version and"
792
                                    " requested ganeti usage: kernel is"
793
                                    " %s.%s, ganeti wants 8.x" %
794
                                    (version['k_major'], version['k_minor']))
795

    
796
    if len(children) not in (0, 2):
797
      raise ValueError("Invalid configuration data %s" % str(children))
798
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 6:
799
      raise ValueError("Invalid configuration data %s" % str(unique_id))
800
    (self._lhost, self._lport,
801
     self._rhost, self._rport,
802
     self._aminor, self._secret) = unique_id
803
    if (self._lhost is not None and self._lhost == self._rhost and
804
        self._lport == self._rport):
805
      raise ValueError("Invalid configuration data, same local/remote %s" %
806
                       (unique_id,))
807
    self.Attach()
808

    
809
  @classmethod
810
  def _InitMeta(cls, minor, dev_path):
811
    """Initialize a meta device.
812

813
    This will not work if the given minor is in use.
814

815
    """
816
    result = utils.RunCmd(["drbdmeta", "--force", cls._DevPath(minor),
817
                           "v08", dev_path, "0", "create-md"])
818
    if result.failed:
819
      raise errors.BlockDeviceError("Can't initialize meta device: %s" %
820
                                    result.output)
821

    
822
  @classmethod
823
  def _FindUnusedMinor(cls):
824
    """Find an unused DRBD device.
825

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

829
    """
830
    data = cls._GetProcData()
831

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

    
850
  @classmethod
851
  def _IsValidMeta(cls, meta_device):
852
    """Check if the given meta device looks like a valid one.
853

854
    """
855
    minor = cls._FindUnusedMinor()
856
    minor_path = cls._DevPath(minor)
857
    result = utils.RunCmd(["drbdmeta", minor_path,
858
                           "v08", meta_device, "0",
859
                           "dstate"])
860
    if result.failed:
861
      logging.error("Invalid meta device %s: %s", meta_device, result.output)
862
      return False
863
    return True
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
      raise errors.BlockDeviceError("Can't parse drbdsetup show output: %s" %
948
                                    str(err))
949

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

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

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

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

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

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

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

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

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

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

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

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

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

1026
    This is the first thing that must be done on an unconfigured DRBD
1027
    device. And it must be done only once.
1028

1029
    """
1030
    if not cls._IsValidMeta(meta):
1031
      return False
1032
    args = ["drbdsetup", cls._DevPath(minor), "disk",
1033
            backend, meta, "0", "-e", "detach", "--create-device"]
1034
    result = utils.RunCmd(args)
1035
    if result.failed:
1036
      logging.error("Can't attach local disk: %s", result.output)
1037
    return not result.failed
1038

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

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

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

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

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

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

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

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

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

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

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

    
1148
  @classmethod
1149
  def _SetMinorSyncSpeed(cls, minor, kbytes):
1150
    """Set the speed of the DRBD syncer.
1151

1152
    This is the low-level implementation.
1153

1154
    @type minor: int
1155
    @param minor: the drbd minor whose settings we change
1156
    @type kbytes: int
1157
    @param kbytes: the speed in kbytes/second
1158
    @rtype: boolean
1159
    @return: the success of the operation
1160

1161
    """
1162
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "syncer",
1163
                           "-r", "%d" % kbytes, "--create-device"])
1164
    if result.failed:
1165
      logging.error("Can't change syncer rate: %s - %s",
1166
                    result.fail_reason, result.output)
1167
    return not result.failed
1168

    
1169
  def SetSyncSpeed(self, kbytes):
1170
    """Set the speed of the DRBD syncer.
1171

1172
    @type kbytes: int
1173
    @param kbytes: the speed in kbytes/second
1174
    @rtype: boolean
1175
    @return: the success of the operation
1176

1177
    """
1178
    if self.minor is None:
1179
      logging.info("Not attached during SetSyncSpeed")
1180
      return False
1181
    children_result = super(DRBD8, self).SetSyncSpeed(kbytes)
1182
    return self._SetMinorSyncSpeed(self.minor, kbytes) and children_result
1183

    
1184
  def GetProcStatus(self):
1185
    """Return device data from /proc.
1186

1187
    """
1188
    if self.minor is None:
1189
      raise errors.BlockDeviceError("GetStats() called while not attached")
1190
    proc_info = self._MassageProcData(self._GetProcData())
1191
    if self.minor not in proc_info:
1192
      raise errors.BlockDeviceError("Can't find myself in /proc (minor %d)" %
1193
                                    self.minor)
1194
    return DRBD8Status(proc_info[self.minor])
1195

    
1196
  def GetSyncStatus(self):
1197
    """Returns the sync status of the device.
1198

1199

1200
    If sync_percent is None, it means all is ok
1201
    If estimated_time is None, it means we can't esimate
1202
    the time needed, otherwise it's the time left in seconds.
1203

1204

1205
    We set the is_degraded parameter to True on two conditions:
1206
    network not connected or local disk missing.
1207

1208
    We compute the ldisk parameter based on wheter we have a local
1209
    disk or not.
1210

1211
    @rtype: tuple
1212
    @return: (sync_percent, estimated_time, is_degraded, ldisk)
1213

1214
    """
1215
    if self.minor is None and not self.Attach():
1216
      raise errors.BlockDeviceError("Can't attach to device in GetSyncStatus")
1217
    stats = self.GetProcStatus()
1218
    ldisk = not stats.is_disk_uptodate
1219
    is_degraded = not stats.is_connected
1220
    return stats.sync_percent, stats.est_time, is_degraded or ldisk, ldisk
1221

    
1222
  def Open(self, force=False):
1223
    """Make the local state primary.
1224

1225
    If the 'force' parameter is given, the '-o' option is passed to
1226
    drbdsetup. Since this is a potentially dangerous operation, the
1227
    force flag should be only given after creation, when it actually
1228
    is mandatory.
1229

1230
    """
1231
    if self.minor is None and not self.Attach():
1232
      logging.error("DRBD cannot attach to a device during open")
1233
      return False
1234
    cmd = ["drbdsetup", self.dev_path, "primary"]
1235
    if force:
1236
      cmd.append("-o")
1237
    result = utils.RunCmd(cmd)
1238
    if result.failed:
1239
      msg = ("Can't make drbd device primary: %s" % result.output)
1240
      logging.error(msg)
1241
      raise errors.BlockDeviceError(msg)
1242

    
1243
  def Close(self):
1244
    """Make the local state secondary.
1245

1246
    This will, of course, fail if the device is in use.
1247

1248
    """
1249
    if self.minor is None and not self.Attach():
1250
      logging.info("Instance not attached to a device")
1251
      raise errors.BlockDeviceError("Can't find device")
1252
    result = utils.RunCmd(["drbdsetup", self.dev_path, "secondary"])
1253
    if result.failed:
1254
      msg = ("Can't switch drbd device to"
1255
             " secondary: %s" % result.output)
1256
      logging.error(msg)
1257
      raise errors.BlockDeviceError(msg)
1258

    
1259
  def DisconnectNet(self):
1260
    """Removes network configuration.
1261

1262
    This method shutdowns the network side of the device.
1263

1264
    The method will wait up to a hardcoded timeout for the device to
1265
    go into standalone after the 'disconnect' command before
1266
    re-configuring it, as sometimes it takes a while for the
1267
    disconnect to actually propagate and thus we might issue a 'net'
1268
    command while the device is still connected. If the device will
1269
    still be attached to the network and we time out, we raise an
1270
    exception.
1271

1272
    """
1273
    if self.minor is None:
1274
      raise errors.BlockDeviceError("DRBD disk not attached in re-attach net")
1275

    
1276
    if None in (self._lhost, self._lport, self._rhost, self._rport):
1277
      raise errors.BlockDeviceError("DRBD disk missing network info in"
1278
                                    " DisconnectNet()")
1279

    
1280
    ever_disconnected = self._ShutdownNet(self.minor)
1281
    timeout_limit = time.time() + self._NET_RECONFIG_TIMEOUT
1282
    sleep_time = 0.100 # we start the retry time at 100 miliseconds
1283
    while time.time() < timeout_limit:
1284
      status = self.GetProcStatus()
1285
      if status.is_standalone:
1286
        break
1287
      # retry the disconnect, it seems possible that due to a
1288
      # well-time disconnect on the peer, my disconnect command might
1289
      # be ingored and forgotten
1290
      ever_disconnected = self._ShutdownNet(self.minor) or ever_disconnected
1291
      time.sleep(sleep_time)
1292
      sleep_time = min(2, sleep_time * 1.5)
1293

    
1294
    if not status.is_standalone:
1295
      if ever_disconnected:
1296
        msg = ("Device did not react to the"
1297
               " 'disconnect' command in a timely manner")
1298
      else:
1299
        msg = ("Can't shutdown network, even after multiple retries")
1300
      raise errors.BlockDeviceError(msg)
1301

    
1302
    reconfig_time = time.time() - timeout_limit + self._NET_RECONFIG_TIMEOUT
1303
    if reconfig_time > 15: # hardcoded alert limit
1304
      logging.debug("DRBD8.DisconnectNet: detach took %.3f seconds",
1305
                    reconfig_time)
1306

    
1307
  def AttachNet(self, multimaster):
1308
    """Reconnects the network.
1309

1310
    This method connects the network side of the device with a
1311
    specified multi-master flag. The device needs to be 'Standalone'
1312
    but have valid network configuration data.
1313

1314
    Args:
1315
      - multimaster: init the network in dual-primary mode
1316

1317
    """
1318
    if self.minor is None:
1319
      raise errors.BlockDeviceError("DRBD disk not attached in AttachNet")
1320

    
1321
    if None in (self._lhost, self._lport, self._rhost, self._rport):
1322
      raise errors.BlockDeviceError("DRBD disk missing network info in"
1323
                                    " AttachNet()")
1324

    
1325
    status = self.GetProcStatus()
1326

    
1327
    if not status.is_standalone:
1328
      raise errors.BlockDeviceError("Device is not standalone in AttachNet")
1329

    
1330
    return self._AssembleNet(self.minor,
1331
                             (self._lhost, self._lport,
1332
                              self._rhost, self._rport),
1333
                             "C", dual_pri=multimaster)
1334

    
1335
  def Attach(self):
1336
    """Check if our minor is configured.
1337

1338
    This doesn't do any device configurations - it only checks if the
1339
    minor is in a state different from Unconfigured.
1340

1341
    Note that this function will not change the state of the system in
1342
    any way (except in case of side-effects caused by reading from
1343
    /proc).
1344

1345
    """
1346
    used_devs = self.GetUsedDevs()
1347
    if self._aminor in used_devs:
1348
      minor = self._aminor
1349
    else:
1350
      minor = None
1351

    
1352
    self._SetFromMinor(minor)
1353
    return minor is not None
1354

    
1355
  def Assemble(self):
1356
    """Assemble the drbd.
1357

1358
    Method:
1359
      - if we have a configured device, we try to ensure that it matches
1360
        our config
1361
      - if not, we create it from zero
1362

1363
    """
1364
    result = super(DRBD8, self).Assemble()
1365
    if not result:
1366
      return result
1367

    
1368
    self.Attach()
1369
    if self.minor is None:
1370
      # local device completely unconfigured
1371
      return self._FastAssemble()
1372
    else:
1373
      # we have to recheck the local and network status and try to fix
1374
      # the device
1375
      return self._SlowAssemble()
1376

    
1377
  def _SlowAssemble(self):
1378
    """Assembles the DRBD device from a (partially) configured device.
1379

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

1384
    """
1385
    for minor in (self._aminor,):
1386
      info = self._GetDevInfo(self._GetShowData(minor))
1387
      match_l = self._MatchesLocal(info)
1388
      match_r = self._MatchesNet(info)
1389
      if match_l and match_r:
1390
        break
1391
      if match_l and not match_r and "local_addr" not in info:
1392
        res_r = self._AssembleNet(minor,
1393
                                  (self._lhost, self._lport,
1394
                                   self._rhost, self._rport),
1395
                                  constants.DRBD_NET_PROTOCOL,
1396
                                  hmac=constants.DRBD_HMAC_ALG,
1397
                                  secret=self._secret
1398
                                  )
1399
        if res_r:
1400
          if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1401
            break
1402
      # the weakest case: we find something that is only net attached
1403
      # even though we were passed some children at init time
1404
      if match_r and "local_dev" not in info:
1405
        break
1406

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

    
1433
    else:
1434
      minor = None
1435

    
1436
    self._SetFromMinor(minor)
1437
    return minor is not None
1438

    
1439
  def _FastAssemble(self):
1440
    """Assemble the drbd device from zero.
1441

1442
    This is run when in Assemble we detect our minor is unused.
1443

1444
    """
1445
    # TODO: maybe completely tear-down the minor (drbdsetup ... down)
1446
    # before attaching our own?
1447
    minor = self._aminor
1448
    need_localdev_teardown = False
1449
    if self._children and self._children[0] and self._children[1]:
1450
      result = self._AssembleLocal(minor, self._children[0].dev_path,
1451
                                   self._children[1].dev_path)
1452
      if not result:
1453
        return False
1454
      need_localdev_teardown = True
1455
    if self._lhost and self._lport and self._rhost and self._rport:
1456
      result = self._AssembleNet(minor,
1457
                                 (self._lhost, self._lport,
1458
                                  self._rhost, self._rport),
1459
                                 constants.DRBD_NET_PROTOCOL,
1460
                                 hmac=constants.DRBD_HMAC_ALG,
1461
                                 secret=self._secret)
1462
      if not result:
1463
        if need_localdev_teardown:
1464
          # we will ignore failures from this
1465
          logging.error("net setup failed, tearing down local device")
1466
          self._ShutdownAll(minor)
1467
        return False
1468
    self._SetFromMinor(minor)
1469
    return True
1470

    
1471
  @classmethod
1472
  def _ShutdownLocal(cls, minor):
1473
    """Detach from the local device.
1474

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

1478
    """
1479
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "detach"])
1480
    if result.failed:
1481
      logging.error("Can't detach local device: %s", result.output)
1482
    return not result.failed
1483

    
1484
  @classmethod
1485
  def _ShutdownNet(cls, minor):
1486
    """Disconnect from the remote peer.
1487

1488
    This fails if we don't have a local device.
1489

1490
    """
1491
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disconnect"])
1492
    if result.failed:
1493
      logging.error("Can't shutdown network: %s", result.output)
1494
    return not result.failed
1495

    
1496
  @classmethod
1497
  def _ShutdownAll(cls, minor):
1498
    """Deactivate the device.
1499

1500
    This will, of course, fail if the device is in use.
1501

1502
    """
1503
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "down"])
1504
    if result.failed:
1505
      logging.error("Can't shutdown drbd device: %s", result.output)
1506
    return not result.failed
1507

    
1508
  def Shutdown(self):
1509
    """Shutdown the DRBD device.
1510

1511
    """
1512
    if self.minor is None and not self.Attach():
1513
      logging.info("DRBD device not attached to a device during Shutdown")
1514
      return True
1515
    if not self._ShutdownAll(self.minor):
1516
      return False
1517
    self.minor = None
1518
    self.dev_path = None
1519
    return True
1520

    
1521
  def Remove(self):
1522
    """Stub remove for DRBD devices.
1523

1524
    """
1525
    return self.Shutdown()
1526

    
1527
  @classmethod
1528
  def Create(cls, unique_id, children, size):
1529
    """Create a new DRBD8 device.
1530

1531
    Since DRBD devices are not created per se, just assembled, this
1532
    function only initializes the metadata.
1533

1534
    """
1535
    if len(children) != 2:
1536
      raise errors.ProgrammerError("Invalid setup for the drbd device")
1537
    # check that the minor is unused
1538
    aminor = unique_id[4]
1539
    proc_info = cls._MassageProcData(cls._GetProcData())
1540
    if aminor in proc_info:
1541
      status = DRBD8Status(proc_info[aminor])
1542
      in_use = status.is_in_use
1543
    else:
1544
      in_use = False
1545
    if in_use:
1546
      raise errors.BlockDeviceError("DRBD minor %d already in use at"
1547
                                    " Create() time" % aminor)
1548
    meta = children[1]
1549
    meta.Assemble()
1550
    if not meta.Attach():
1551
      raise errors.BlockDeviceError("Can't attach to meta device")
1552
    if not cls._CheckMetaSize(meta.dev_path):
1553
      raise errors.BlockDeviceError("Invalid meta device size")
1554
    cls._InitMeta(cls._FindUnusedMinor(), meta.dev_path)
1555
    if not cls._IsValidMeta(meta.dev_path):
1556
      raise errors.BlockDeviceError("Cannot initalize meta device")
1557
    return cls(unique_id, children)
1558

    
1559
  def Grow(self, amount):
1560
    """Resize the DRBD device and its backing storage.
1561

1562
    """
1563
    if self.minor is None:
1564
      raise errors.ProgrammerError("drbd8: Grow called while not attached")
1565
    if len(self._children) != 2 or None in self._children:
1566
      raise errors.BlockDeviceError("Cannot grow diskless DRBD8 device")
1567
    self._children[0].Grow(amount)
1568
    result = utils.RunCmd(["drbdsetup", self.dev_path, "resize"])
1569
    if result.failed:
1570
      raise errors.BlockDeviceError("resize failed for %s: %s" %
1571
                                    (self.dev_path, result.output))
1572
    return
1573

    
1574

    
1575
class FileStorage(BlockDev):
1576
  """File device.
1577

1578
  This class represents the a file storage backend device.
1579

1580
  The unique_id for the file device is a (file_driver, file_path) tuple.
1581

1582
  """
1583
  def __init__(self, unique_id, children):
1584
    """Initalizes a file device backend.
1585

1586
    """
1587
    if children:
1588
      raise errors.BlockDeviceError("Invalid setup for file device")
1589
    super(FileStorage, self).__init__(unique_id, children)
1590
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1591
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1592
    self.driver = unique_id[0]
1593
    self.dev_path = unique_id[1]
1594
    self.Attach()
1595

    
1596
  def Assemble(self):
1597
    """Assemble the device.
1598

1599
    Checks whether the file device exists, raises BlockDeviceError otherwise.
1600

1601
    """
1602
    if not os.path.exists(self.dev_path):
1603
      raise errors.BlockDeviceError("File device '%s' does not exist." %
1604
                                    self.dev_path)
1605
    return True
1606

    
1607
  def Shutdown(self):
1608
    """Shutdown the device.
1609

1610
    This is a no-op for the file type, as we don't deacivate
1611
    the file on shutdown.
1612

1613
    """
1614
    return True
1615

    
1616
  def Open(self, force=False):
1617
    """Make the device ready for I/O.
1618

1619
    This is a no-op for the file type.
1620

1621
    """
1622
    pass
1623

    
1624
  def Close(self):
1625
    """Notifies that the device will no longer be used for I/O.
1626

1627
    This is a no-op for the file type.
1628

1629
    """
1630
    pass
1631

    
1632
  def Remove(self):
1633
    """Remove the file backing the block device.
1634

1635
    @rtype: boolean
1636
    @return: True if the removal was successful
1637

1638
    """
1639
    if not os.path.exists(self.dev_path):
1640
      return True
1641
    try:
1642
      os.remove(self.dev_path)
1643
      return True
1644
    except OSError, err:
1645
      logging.error("Can't remove file '%s': %s", self.dev_path, err)
1646
      return False
1647

    
1648
  def Attach(self):
1649
    """Attach to an existing file.
1650

1651
    Check if this file already exists.
1652

1653
    @rtype: boolean
1654
    @return: True if file exists
1655

1656
    """
1657
    self.attached = os.path.exists(self.dev_path)
1658
    return self.attached
1659

    
1660
  @classmethod
1661
  def Create(cls, unique_id, children, size):
1662
    """Create a new file.
1663

1664
    @param size: the size of file in MiB
1665

1666
    @rtype: L{bdev.FileStorage}
1667
    @return: an instance of FileStorage
1668

1669
    """
1670
    # TODO: decide whether we should check for existing files and
1671
    # abort or not
1672
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1673
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1674
    dev_path = unique_id[1]
1675
    try:
1676
      f = open(dev_path, 'w')
1677
      f.truncate(size * 1024 * 1024)
1678
      f.close()
1679
    except IOError, err:
1680
      raise errors.BlockDeviceError("Error in file creation: %" % str(err))
1681

    
1682
    return FileStorage(unique_id, children)
1683

    
1684

    
1685
DEV_MAP = {
1686
  constants.LD_LV: LogicalVolume,
1687
  constants.LD_DRBD8: DRBD8,
1688
  constants.LD_FILE: FileStorage,
1689
  }
1690

    
1691

    
1692
def FindDevice(dev_type, unique_id, children):
1693
  """Search for an existing, assembled device.
1694

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

1698
  """
1699
  if dev_type not in DEV_MAP:
1700
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1701
  device = DEV_MAP[dev_type](unique_id, children)
1702
  if not device.attached:
1703
    return None
1704
  return device
1705

    
1706

    
1707
def Assemble(dev_type, unique_id, children):
1708
  """Try to attach or assemble an existing device.
1709

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

1713
  """
1714
  if dev_type not in DEV_MAP:
1715
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1716
  device = DEV_MAP[dev_type](unique_id, children)
1717
  if not device.Assemble():
1718
    raise errors.BlockDeviceError("Can't find a valid block device for"
1719
                                  " %s/%s/%s" %
1720
                                  (dev_type, unique_id, children))
1721
  return device
1722

    
1723

    
1724
def Create(dev_type, unique_id, children, size):
1725
  """Create a device.
1726

1727
  """
1728
  if dev_type not in DEV_MAP:
1729
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1730
  device = DEV_MAP[dev_type].Create(unique_id, children, size)
1731
  return device