Statistics
| Branch: | Tag: | Revision:

root / lib / bdev.py @ 3b559640

History | View | Annotate | Download (54.1 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 _GetShowParser(cls):
852
    """Return a parser for `drbd show` output.
853

854
    This will either create or return an already-create parser for the
855
    output of the command `drbd show`.
856

857
    """
858
    if cls._PARSE_SHOW is not None:
859
      return cls._PARSE_SHOW
860

    
861
    # pyparsing setup
862
    lbrace = pyp.Literal("{").suppress()
863
    rbrace = pyp.Literal("}").suppress()
864
    semi = pyp.Literal(";").suppress()
865
    # this also converts the value to an int
866
    number = pyp.Word(pyp.nums).setParseAction(lambda s, l, t: int(t[0]))
867

    
868
    comment = pyp.Literal ("#") + pyp.Optional(pyp.restOfLine)
869
    defa = pyp.Literal("_is_default").suppress()
870
    dbl_quote = pyp.Literal('"').suppress()
871

    
872
    keyword = pyp.Word(pyp.alphanums + '-')
873

    
874
    # value types
875
    value = pyp.Word(pyp.alphanums + '_-/.:')
876
    quoted = dbl_quote + pyp.CharsNotIn('"') + dbl_quote
877
    addr_port = (pyp.Word(pyp.nums + '.') + pyp.Literal(':').suppress() +
878
                 number)
879
    # meta device, extended syntax
880
    meta_value = ((value ^ quoted) + pyp.Literal('[').suppress() +
881
                  number + pyp.Word(']').suppress())
882

    
883
    # a statement
884
    stmt = (~rbrace + keyword + ~lbrace +
885
            pyp.Optional(addr_port ^ value ^ quoted ^ meta_value) +
886
            pyp.Optional(defa) + semi +
887
            pyp.Optional(pyp.restOfLine).suppress())
888

    
889
    # an entire section
890
    section_name = pyp.Word(pyp.alphas + '_')
891
    section = section_name + lbrace + pyp.ZeroOrMore(pyp.Group(stmt)) + rbrace
892

    
893
    bnf = pyp.ZeroOrMore(pyp.Group(section ^ stmt))
894
    bnf.ignore(comment)
895

    
896
    cls._PARSE_SHOW = bnf
897

    
898
    return bnf
899

    
900
  @classmethod
901
  def _GetShowData(cls, minor):
902
    """Return the `drbdsetup show` data for a minor.
903

904
    """
905
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "show"])
906
    if result.failed:
907
      logging.error("Can't display the drbd config: %s - %s",
908
                    result.fail_reason, result.output)
909
      return None
910
    return result.stdout
911

    
912
  @classmethod
913
  def _GetDevInfo(cls, out):
914
    """Parse details about a given DRBD minor.
915

916
    This return, if available, the local backing device (as a path)
917
    and the local and remote (ip, port) information from a string
918
    containing the output of the `drbdsetup show` command as returned
919
    by _GetShowData.
920

921
    """
922
    data = {}
923
    if not out:
924
      return data
925

    
926
    bnf = cls._GetShowParser()
927
    # run pyparse
928

    
929
    try:
930
      results = bnf.parseString(out)
931
    except pyp.ParseException, err:
932
      raise errors.BlockDeviceError("Can't parse drbdsetup show output: %s" %
933
                                    str(err))
934

    
935
    # and massage the results into our desired format
936
    for section in results:
937
      sname = section[0]
938
      if sname == "_this_host":
939
        for lst in section[1:]:
940
          if lst[0] == "disk":
941
            data["local_dev"] = lst[1]
942
          elif lst[0] == "meta-disk":
943
            data["meta_dev"] = lst[1]
944
            data["meta_index"] = lst[2]
945
          elif lst[0] == "address":
946
            data["local_addr"] = tuple(lst[1:])
947
      elif sname == "_remote_host":
948
        for lst in section[1:]:
949
          if lst[0] == "address":
950
            data["remote_addr"] = tuple(lst[1:])
951
    return data
952

    
953
  def _MatchesLocal(self, info):
954
    """Test if our local config matches with an existing device.
955

956
    The parameter should be as returned from `_GetDevInfo()`. This
957
    method tests if our local backing device is the same as the one in
958
    the info parameter, in effect testing if we look like the given
959
    device.
960

961
    """
962
    if self._children:
963
      backend, meta = self._children
964
    else:
965
      backend = meta = None
966

    
967
    if backend is not None:
968
      retval = ("local_dev" in info and info["local_dev"] == backend.dev_path)
969
    else:
970
      retval = ("local_dev" not in info)
971

    
972
    if meta is not None:
973
      retval = retval and ("meta_dev" in info and
974
                           info["meta_dev"] == meta.dev_path)
975
      retval = retval and ("meta_index" in info and
976
                           info["meta_index"] == 0)
977
    else:
978
      retval = retval and ("meta_dev" not in info and
979
                           "meta_index" not in info)
980
    return retval
981

    
982
  def _MatchesNet(self, info):
983
    """Test if our network config matches with an existing device.
984

985
    The parameter should be as returned from `_GetDevInfo()`. This
986
    method tests if our network configuration is the same as the one
987
    in the info parameter, in effect testing if we look like the given
988
    device.
989

990
    """
991
    if (((self._lhost is None and not ("local_addr" in info)) and
992
         (self._rhost is None and not ("remote_addr" in info)))):
993
      return True
994

    
995
    if self._lhost is None:
996
      return False
997

    
998
    if not ("local_addr" in info and
999
            "remote_addr" in info):
1000
      return False
1001

    
1002
    retval = (info["local_addr"] == (self._lhost, self._lport))
1003
    retval = (retval and
1004
              info["remote_addr"] == (self._rhost, self._rport))
1005
    return retval
1006

    
1007
  @classmethod
1008
  def _AssembleLocal(cls, minor, backend, meta):
1009
    """Configure the local part of a DRBD device.
1010

1011
    """
1012
    args = ["drbdsetup", cls._DevPath(minor), "disk",
1013
            backend, meta, "0", "-e", "detach", "--create-device"]
1014
    result = utils.RunCmd(args)
1015
    if result.failed:
1016
      logging.error("Can't attach local disk: %s", result.output)
1017
    return not result.failed
1018

    
1019
  @classmethod
1020
  def _AssembleNet(cls, minor, net_info, protocol,
1021
                   dual_pri=False, hmac=None, secret=None):
1022
    """Configure the network part of the device.
1023

1024
    """
1025
    lhost, lport, rhost, rport = net_info
1026
    if None in net_info:
1027
      # we don't want network connection and actually want to make
1028
      # sure its shutdown
1029
      return cls._ShutdownNet(minor)
1030

    
1031
    # Workaround for a race condition. When DRBD is doing its dance to
1032
    # establish a connection with its peer, it also sends the
1033
    # synchronization speed over the wire. In some cases setting the
1034
    # sync speed only after setting up both sides can race with DRBD
1035
    # connecting, hence we set it here before telling DRBD anything
1036
    # about its peer.
1037
    cls._SetMinorSyncSpeed(minor, constants.SYNC_SPEED)
1038

    
1039
    args = ["drbdsetup", cls._DevPath(minor), "net",
1040
            "%s:%s" % (lhost, lport), "%s:%s" % (rhost, rport), protocol,
1041
            "-A", "discard-zero-changes",
1042
            "-B", "consensus",
1043
            "--create-device",
1044
            ]
1045
    if dual_pri:
1046
      args.append("-m")
1047
    if hmac and secret:
1048
      args.extend(["-a", hmac, "-x", secret])
1049
    result = utils.RunCmd(args)
1050
    if result.failed:
1051
      logging.error("Can't setup network for dbrd device: %s - %s",
1052
                    result.fail_reason, result.output)
1053
      return False
1054

    
1055
    timeout = time.time() + 10
1056
    ok = False
1057
    while time.time() < timeout:
1058
      info = cls._GetDevInfo(cls._GetShowData(minor))
1059
      if not "local_addr" in info or not "remote_addr" in info:
1060
        time.sleep(1)
1061
        continue
1062
      if (info["local_addr"] != (lhost, lport) or
1063
          info["remote_addr"] != (rhost, rport)):
1064
        time.sleep(1)
1065
        continue
1066
      ok = True
1067
      break
1068
    if not ok:
1069
      logging.error("Timeout while configuring network")
1070
      return False
1071
    return True
1072

    
1073
  def AddChildren(self, devices):
1074
    """Add a disk to the DRBD device.
1075

1076
    """
1077
    if self.minor is None:
1078
      raise errors.BlockDeviceError("Can't attach to dbrd8 during AddChildren")
1079
    if len(devices) != 2:
1080
      raise errors.BlockDeviceError("Need two devices for AddChildren")
1081
    info = self._GetDevInfo(self._GetShowData(self.minor))
1082
    if "local_dev" in info:
1083
      raise errors.BlockDeviceError("DRBD8 already attached to a local disk")
1084
    backend, meta = devices
1085
    if backend.dev_path is None or meta.dev_path is None:
1086
      raise errors.BlockDeviceError("Children not ready during AddChildren")
1087
    backend.Open()
1088
    meta.Open()
1089
    if not self._CheckMetaSize(meta.dev_path):
1090
      raise errors.BlockDeviceError("Invalid meta device size")
1091
    self._InitMeta(self._FindUnusedMinor(), meta.dev_path)
1092

    
1093
    if not self._AssembleLocal(self.minor, backend.dev_path, meta.dev_path):
1094
      raise errors.BlockDeviceError("Can't attach to local storage")
1095
    self._children = devices
1096

    
1097
  def RemoveChildren(self, devices):
1098
    """Detach the drbd device from local storage.
1099

1100
    """
1101
    if self.minor is None:
1102
      raise errors.BlockDeviceError("Can't attach to drbd8 during"
1103
                                    " RemoveChildren")
1104
    # early return if we don't actually have backing storage
1105
    info = self._GetDevInfo(self._GetShowData(self.minor))
1106
    if "local_dev" not in info:
1107
      return
1108
    if len(self._children) != 2:
1109
      raise errors.BlockDeviceError("We don't have two children: %s" %
1110
                                    self._children)
1111
    if self._children.count(None) == 2: # we don't actually have children :)
1112
      logging.error("Requested detach while detached")
1113
      return
1114
    if len(devices) != 2:
1115
      raise errors.BlockDeviceError("We need two children in RemoveChildren")
1116
    for child, dev in zip(self._children, devices):
1117
      if dev != child.dev_path:
1118
        raise errors.BlockDeviceError("Mismatch in local storage"
1119
                                      " (%s != %s) in RemoveChildren" %
1120
                                      (dev, child.dev_path))
1121

    
1122
    if not self._ShutdownLocal(self.minor):
1123
      raise errors.BlockDeviceError("Can't detach from local storage")
1124
    self._children = []
1125

    
1126
  @classmethod
1127
  def _SetMinorSyncSpeed(cls, minor, kbytes):
1128
    """Set the speed of the DRBD syncer.
1129

1130
    This is the low-level implementation.
1131

1132
    @type minor: int
1133
    @param minor: the drbd minor whose settings we change
1134
    @type kbytes: int
1135
    @param kbytes: the speed in kbytes/second
1136
    @rtype: boolean
1137
    @return: the success of the operation
1138

1139
    """
1140
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "syncer",
1141
                           "-r", "%d" % kbytes, "--create-device"])
1142
    if result.failed:
1143
      logging.error("Can't change syncer rate: %s - %s",
1144
                    result.fail_reason, result.output)
1145
    return not result.failed
1146

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

1150
    @type kbytes: int
1151
    @param kbytes: the speed in kbytes/second
1152
    @rtype: boolean
1153
    @return: the success of the operation
1154

1155
    """
1156
    if self.minor is None:
1157
      logging.info("Not attached during SetSyncSpeed")
1158
      return False
1159
    children_result = super(DRBD8, self).SetSyncSpeed(kbytes)
1160
    return self._SetMinorSyncSpeed(self.minor, kbytes) and children_result
1161

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

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

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

1177

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

1182

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

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

1189
    @rtype: tuple
1190
    @return: (sync_percent, estimated_time, is_degraded, ldisk)
1191

1192
    """
1193
    if self.minor is None and not self.Attach():
1194
      raise errors.BlockDeviceError("Can't attach to device in GetSyncStatus")
1195
    stats = self.GetProcStatus()
1196
    ldisk = not stats.is_disk_uptodate
1197
    is_degraded = not stats.is_connected
1198
    return stats.sync_percent, stats.est_time, is_degraded or ldisk, ldisk
1199

    
1200
  def Open(self, force=False):
1201
    """Make the local state primary.
1202

1203
    If the 'force' parameter is given, the '-o' option is passed to
1204
    drbdsetup. Since this is a potentially dangerous operation, the
1205
    force flag should be only given after creation, when it actually
1206
    is mandatory.
1207

1208
    """
1209
    if self.minor is None and not self.Attach():
1210
      logging.error("DRBD cannot attach to a device during open")
1211
      return False
1212
    cmd = ["drbdsetup", self.dev_path, "primary"]
1213
    if force:
1214
      cmd.append("-o")
1215
    result = utils.RunCmd(cmd)
1216
    if result.failed:
1217
      msg = ("Can't make drbd device primary: %s" % result.output)
1218
      logging.error(msg)
1219
      raise errors.BlockDeviceError(msg)
1220

    
1221
  def Close(self):
1222
    """Make the local state secondary.
1223

1224
    This will, of course, fail if the device is in use.
1225

1226
    """
1227
    if self.minor is None and not self.Attach():
1228
      logging.info("Instance not attached to a device")
1229
      raise errors.BlockDeviceError("Can't find device")
1230
    result = utils.RunCmd(["drbdsetup", self.dev_path, "secondary"])
1231
    if result.failed:
1232
      msg = ("Can't switch drbd device to"
1233
             " secondary: %s" % result.output)
1234
      logging.error(msg)
1235
      raise errors.BlockDeviceError(msg)
1236

    
1237
  def DisconnectNet(self):
1238
    """Removes network configuration.
1239

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

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

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

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

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

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

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

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

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

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

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

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

    
1303
    status = self.GetProcStatus()
1304

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

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

    
1313
  def Attach(self):
1314
    """Check if our minor is configured.
1315

1316
    This doesn't do any device configurations - it only checks if the
1317
    minor is in a state different from Unconfigured.
1318

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

1323
    """
1324
    used_devs = self.GetUsedDevs()
1325
    if self._aminor in used_devs:
1326
      minor = self._aminor
1327
    else:
1328
      minor = None
1329

    
1330
    self._SetFromMinor(minor)
1331
    return minor is not None
1332

    
1333
  def Assemble(self):
1334
    """Assemble the drbd.
1335

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

1341
    """
1342
    result = super(DRBD8, self).Assemble()
1343
    if not result:
1344
      return result
1345

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

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

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

1362
    """
1363
    for minor in (self._aminor,):
1364
      info = self._GetDevInfo(self._GetShowData(minor))
1365
      match_l = self._MatchesLocal(info)
1366
      match_r = self._MatchesNet(info)
1367
      if match_l and match_r:
1368
        break
1369
      if match_l and not match_r and "local_addr" not in info:
1370
        res_r = self._AssembleNet(minor,
1371
                                  (self._lhost, self._lport,
1372
                                   self._rhost, self._rport),
1373
                                  constants.DRBD_NET_PROTOCOL,
1374
                                  hmac=constants.DRBD_HMAC_ALG,
1375
                                  secret=self._secret
1376
                                  )
1377
        if res_r:
1378
          if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1379
            break
1380
      # the weakest case: we find something that is only net attached
1381
      # even though we were passed some children at init time
1382
      if match_r and "local_dev" not in info:
1383
        break
1384

    
1385
      # this case must be considered only if we actually have local
1386
      # storage, i.e. not in diskless mode, because all diskless
1387
      # devices are equal from the point of view of local
1388
      # configuration
1389
      if (match_l and "local_dev" in info and
1390
          not match_r and "local_addr" in info):
1391
        # strange case - the device network part points to somewhere
1392
        # else, even though its local storage is ours; as we own the
1393
        # drbd space, we try to disconnect from the remote peer and
1394
        # reconnect to our correct one
1395
        if not self._ShutdownNet(minor):
1396
          raise errors.BlockDeviceError("Device has correct local storage,"
1397
                                        " wrong remote peer and is unable to"
1398
                                        " disconnect in order to attach to"
1399
                                        " the correct peer")
1400
        # note: _AssembleNet also handles the case when we don't want
1401
        # local storage (i.e. one or more of the _[lr](host|port) is
1402
        # None)
1403
        if (self._AssembleNet(minor, (self._lhost, self._lport,
1404
                                      self._rhost, self._rport),
1405
                              constants.DRBD_NET_PROTOCOL,
1406
                              hmac=constants.DRBD_HMAC_ALG,
1407
                              secret=self._secret) and
1408
            self._MatchesNet(self._GetDevInfo(self._GetShowData(minor)))):
1409
          break
1410

    
1411
    else:
1412
      minor = None
1413

    
1414
    self._SetFromMinor(minor)
1415
    return minor is not None
1416

    
1417
  def _FastAssemble(self):
1418
    """Assemble the drbd device from zero.
1419

1420
    This is run when in Assemble we detect our minor is unused.
1421

1422
    """
1423
    # TODO: maybe completely tear-down the minor (drbdsetup ... down)
1424
    # before attaching our own?
1425
    minor = self._aminor
1426
    need_localdev_teardown = False
1427
    if self._children and self._children[0] and self._children[1]:
1428
      result = self._AssembleLocal(minor, self._children[0].dev_path,
1429
                                   self._children[1].dev_path)
1430
      if not result:
1431
        return False
1432
      need_localdev_teardown = True
1433
    if self._lhost and self._lport and self._rhost and self._rport:
1434
      result = self._AssembleNet(minor,
1435
                                 (self._lhost, self._lport,
1436
                                  self._rhost, self._rport),
1437
                                 constants.DRBD_NET_PROTOCOL,
1438
                                 hmac=constants.DRBD_HMAC_ALG,
1439
                                 secret=self._secret)
1440
      if not result:
1441
        if need_localdev_teardown:
1442
          # we will ignore failures from this
1443
          logging.error("net setup failed, tearing down local device")
1444
          self._ShutdownAll(minor)
1445
        return False
1446
    self._SetFromMinor(minor)
1447
    return True
1448

    
1449
  @classmethod
1450
  def _ShutdownLocal(cls, minor):
1451
    """Detach from the local device.
1452

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

1456
    """
1457
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "detach"])
1458
    if result.failed:
1459
      logging.error("Can't detach local device: %s", result.output)
1460
    return not result.failed
1461

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

1466
    This fails if we don't have a local device.
1467

1468
    """
1469
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disconnect"])
1470
    if result.failed:
1471
      logging.error("Can't shutdown network: %s", result.output)
1472
    return not result.failed
1473

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

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

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

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

1489
    """
1490
    if self.minor is None and not self.Attach():
1491
      logging.info("DRBD device not attached to a device during Shutdown")
1492
      return True
1493
    if not self._ShutdownAll(self.minor):
1494
      return False
1495
    self.minor = None
1496
    self.dev_path = None
1497
    return True
1498

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

1502
    """
1503
    return self.Shutdown()
1504

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

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

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

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

1538
    """
1539
    if self.minor is None:
1540
      raise errors.ProgrammerError("drbd8: Grow called while not attached")
1541
    if len(self._children) != 2 or None in self._children:
1542
      raise errors.BlockDeviceError("Cannot grow diskless DRBD8 device")
1543
    self._children[0].Grow(amount)
1544
    result = utils.RunCmd(["drbdsetup", self.dev_path, "resize"])
1545
    if result.failed:
1546
      raise errors.BlockDeviceError("resize failed for %s: %s" %
1547
                                    (self.dev_path, result.output))
1548
    return
1549

    
1550

    
1551
class FileStorage(BlockDev):
1552
  """File device.
1553

1554
  This class represents the a file storage backend device.
1555

1556
  The unique_id for the file device is a (file_driver, file_path) tuple.
1557

1558
  """
1559
  def __init__(self, unique_id, children):
1560
    """Initalizes a file device backend.
1561

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

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

1575
    Checks whether the file device exists, raises BlockDeviceError otherwise.
1576

1577
    """
1578
    if not os.path.exists(self.dev_path):
1579
      raise errors.BlockDeviceError("File device '%s' does not exist." %
1580
                                    self.dev_path)
1581
    return True
1582

    
1583
  def Shutdown(self):
1584
    """Shutdown the device.
1585

1586
    This is a no-op for the file type, as we don't deacivate
1587
    the file on shutdown.
1588

1589
    """
1590
    return True
1591

    
1592
  def Open(self, force=False):
1593
    """Make the device ready for I/O.
1594

1595
    This is a no-op for the file type.
1596

1597
    """
1598
    pass
1599

    
1600
  def Close(self):
1601
    """Notifies that the device will no longer be used for I/O.
1602

1603
    This is a no-op for the file type.
1604

1605
    """
1606
    pass
1607

    
1608
  def Remove(self):
1609
    """Remove the file backing the block device.
1610

1611
    @rtype: boolean
1612
    @return: True if the removal was successful
1613

1614
    """
1615
    if not os.path.exists(self.dev_path):
1616
      return True
1617
    try:
1618
      os.remove(self.dev_path)
1619
      return True
1620
    except OSError, err:
1621
      logging.error("Can't remove file '%s': %s", self.dev_path, err)
1622
      return False
1623

    
1624
  def Attach(self):
1625
    """Attach to an existing file.
1626

1627
    Check if this file already exists.
1628

1629
    @rtype: boolean
1630
    @return: True if file exists
1631

1632
    """
1633
    self.attached = os.path.exists(self.dev_path)
1634
    return self.attached
1635

    
1636
  @classmethod
1637
  def Create(cls, unique_id, children, size):
1638
    """Create a new file.
1639

1640
    @param size: the size of file in MiB
1641

1642
    @rtype: L{bdev.FileStorage}
1643
    @return: an instance of FileStorage
1644

1645
    """
1646
    # TODO: decide whether we should check for existing files and
1647
    # abort or not
1648
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1649
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1650
    dev_path = unique_id[1]
1651
    try:
1652
      f = open(dev_path, 'w')
1653
      f.truncate(size * 1024 * 1024)
1654
      f.close()
1655
    except IOError, err:
1656
      raise errors.BlockDeviceError("Error in file creation: %" % str(err))
1657

    
1658
    return FileStorage(unique_id, children)
1659

    
1660

    
1661
DEV_MAP = {
1662
  constants.LD_LV: LogicalVolume,
1663
  constants.LD_DRBD8: DRBD8,
1664
  constants.LD_FILE: FileStorage,
1665
  }
1666

    
1667

    
1668
def FindDevice(dev_type, unique_id, children):
1669
  """Search for an existing, assembled device.
1670

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

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

    
1682

    
1683
def Assemble(dev_type, unique_id, children):
1684
  """Try to attach or assemble an existing device.
1685

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

1689
  """
1690
  if dev_type not in DEV_MAP:
1691
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1692
  device = DEV_MAP[dev_type](unique_id, children)
1693
  if not device.Assemble():
1694
    raise errors.BlockDeviceError("Can't find a valid block device for"
1695
                                  " %s/%s/%s" %
1696
                                  (dev_type, unique_id, children))
1697
  return device
1698

    
1699

    
1700
def Create(dev_type, unique_id, children, size):
1701
  """Create a device.
1702

1703
  """
1704
  if dev_type not in DEV_MAP:
1705
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1706
  device = DEV_MAP[dev_type].Create(unique_id, children, size)
1707
  return device