Statistics
| Branch: | Tag: | Revision:

root / lib / bdev.py @ 3c003d9d

History | View | Annotate | Download (54.8 kB)

1
#
2
#
3

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

    
21

    
22
"""Block device abstraction"""
23

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

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

    
35

    
36
def _IgnoreError(fn, *args, **kwargs):
37
  """Executes the given function, ignoring BlockDeviceErrors.
38

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

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

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

    
53

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

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

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

    
67

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

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

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

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

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

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

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

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

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

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

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

131
    """
132
    pass
133

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

137
    """
138
    raise NotImplementedError
139

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

143
    """
144
    raise NotImplementedError
145

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

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

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

157
    """
158
    raise NotImplementedError
159

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

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

167
    """
168
    raise NotImplementedError
169

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

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

175
    """
176
    raise NotImplementedError
177

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

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

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

187
    """
188
    raise NotImplementedError
189

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

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

197
    """
198
    raise NotImplementedError
199

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

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

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

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

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

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

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

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

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

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

234
    """
235
    return None, None, False, False
236

    
237

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

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

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

    
262

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

266
    Only supported for some device types.
267

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

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

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

277
    """
278
    raise NotImplementedError
279

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

    
285

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

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

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

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

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

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

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

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

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

339
    @param vg_name: the volume group name
340

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

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

    
364
    return data
365

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

453
    """
454
    pass
455

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

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

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

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

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

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

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

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

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

486
    """
487
    pass
488

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

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

494
    """
495
    pass
496

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

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

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

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

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

    
523
    return snap_name
524

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

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

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

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

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

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

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

    
559

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

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

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

    
572
  CS_UNCONFIGURED = "Unconfigured"
573
  CS_STANDALONE = "StandAlone"
574
  CS_WFCONNECTION = "WFConnection"
575
  CS_WFREPORTPARAMS = "WFReportParams"
576
  CS_CONNECTED = "Connected"
577
  CS_STARTINGSYNCS = "StartingSyncS"
578
  CS_STARTINGSYNCT = "StartingSyncT"
579
  CS_WFBITMAPS = "WFBitMapS"
580
  CS_WFBITMAPT = "WFBitMapT"
581
  CS_WFSYNCUUID = "WFSyncUUID"
582
  CS_SYNCSOURCE = "SyncSource"
583
  CS_SYNCTARGET = "SyncTarget"
584
  CS_PAUSEDSYNCS = "PausedSyncS"
585
  CS_PAUSEDSYNCT = "PausedSyncT"
586
  CSET_SYNC = frozenset([
587
    CS_WFREPORTPARAMS,
588
    CS_STARTINGSYNCS,
589
    CS_STARTINGSYNCT,
590
    CS_WFBITMAPS,
591
    CS_WFBITMAPT,
592
    CS_WFSYNCUUID,
593
    CS_SYNCSOURCE,
594
    CS_SYNCTARGET,
595
    CS_PAUSEDSYNCS,
596
    CS_PAUSEDSYNCT,
597
    ])
598

    
599
  DS_DISKLESS = "Diskless"
600
  DS_ATTACHING = "Attaching" # transient state
601
  DS_FAILED = "Failed" # transient state, next: diskless
602
  DS_NEGOTIATING = "Negotiating" # transient state
603
  DS_INCONSISTENT = "Inconsistent" # while syncing or after creation
604
  DS_OUTDATED = "Outdated"
605
  DS_DUNKNOWN = "DUnknown" # shown for peer disk when not connected
606
  DS_CONSISTENT = "Consistent"
607
  DS_UPTODATE = "UpToDate" # normal state
608

    
609
  RO_PRIMARY = "Primary"
610
  RO_SECONDARY = "Secondary"
611
  RO_UNKNOWN = "Unknown"
612

    
613
  def __init__(self, procline):
614
    u = self.UNCONF_RE.match(procline)
615
    if u:
616
      self.cstatus = self.CS_UNCONFIGURED
617
      self.lrole = self.rrole = self.ldisk = self.rdisk = None
618
    else:
619
      m = self.LINE_RE.match(procline)
620
      if not m:
621
        raise errors.BlockDeviceError("Can't parse input data '%s'" % procline)
622
      self.cstatus = m.group(1)
623
      self.lrole = m.group(2)
624
      self.rrole = m.group(3)
625
      self.ldisk = m.group(4)
626
      self.rdisk = m.group(5)
627

    
628
    # end reading of data from the LINE_RE or UNCONF_RE
629

    
630
    self.is_standalone = self.cstatus == self.CS_STANDALONE
631
    self.is_wfconn = self.cstatus == self.CS_WFCONNECTION
632
    self.is_connected = self.cstatus == self.CS_CONNECTED
633
    self.is_primary = self.lrole == self.RO_PRIMARY
634
    self.is_secondary = self.lrole == self.RO_SECONDARY
635
    self.peer_primary = self.rrole == self.RO_PRIMARY
636
    self.peer_secondary = self.rrole == self.RO_SECONDARY
637
    self.both_primary = self.is_primary and self.peer_primary
638
    self.both_secondary = self.is_secondary and self.peer_secondary
639

    
640
    self.is_diskless = self.ldisk == self.DS_DISKLESS
641
    self.is_disk_uptodate = self.ldisk == self.DS_UPTODATE
642

    
643
    self.is_in_resync = self.cstatus in self.CSET_SYNC
644
    self.is_in_use = self.cstatus != self.CS_UNCONFIGURED
645

    
646
    m = self.SYNC_RE.match(procline)
647
    if m:
648
      self.sync_percent = float(m.group(1))
649
      hours = int(m.group(2))
650
      minutes = int(m.group(3))
651
      seconds = int(m.group(4))
652
      self.est_time = hours * 3600 + minutes * 60 + seconds
653
    else:
654
      # we have (in this if branch) no percent information, but if
655
      # we're resyncing we need to 'fake' a sync percent information,
656
      # as this is how cmdlib determines if it makes sense to wait for
657
      # resyncing or not
658
      if self.is_in_resync:
659
        self.sync_percent = 0
660
      else:
661
        self.sync_percent = None
662
      self.est_time = None
663

    
664

    
665
class BaseDRBD(BlockDev):
666
  """Base DRBD class.
667

668
  This class contains a few bits of common functionality between the
669
  0.7 and 8.x versions of DRBD.
670

671
  """
672
  _VERSION_RE = re.compile(r"^version: (\d+)\.(\d+)\.(\d+)"
673
                           r" \(api:(\d+)/proto:(\d+)(?:-(\d+))?\)")
674

    
675
  _DRBD_MAJOR = 147
676
  _ST_UNCONFIGURED = "Unconfigured"
677
  _ST_WFCONNECTION = "WFConnection"
678
  _ST_CONNECTED = "Connected"
679

    
680
  _STATUS_FILE = "/proc/drbd"
681

    
682
  @staticmethod
683
  def _GetProcData(filename=_STATUS_FILE):
684
    """Return data from /proc/drbd.
685

686
    """
687
    try:
688
      stat = open(filename, "r")
689
      try:
690
        data = stat.read().splitlines()
691
      finally:
692
        stat.close()
693
    except EnvironmentError, err:
694
      if err.errno == errno.ENOENT:
695
        _ThrowError("The file %s cannot be opened, check if the module"
696
                    " is loaded (%s)", filename, str(err))
697
      else:
698
        _ThrowError("Can't read the DRBD proc file %s: %s", filename, str(err))
699
    if not data:
700
      _ThrowError("Can't read any data from %s", filename)
701
    return data
702

    
703
  @staticmethod
704
  def _MassageProcData(data):
705
    """Transform the output of _GetProdData into a nicer form.
706

707
    @return: a dictionary of minor: joined lines from /proc/drbd
708
        for that minor
709

710
    """
711
    lmatch = re.compile("^ *([0-9]+):.*$")
712
    results = {}
713
    old_minor = old_line = None
714
    for line in data:
715
      lresult = lmatch.match(line)
716
      if lresult is not None:
717
        if old_minor is not None:
718
          results[old_minor] = old_line
719
        old_minor = int(lresult.group(1))
720
        old_line = line
721
      else:
722
        if old_minor is not None:
723
          old_line += " " + line.strip()
724
    # add last line
725
    if old_minor is not None:
726
      results[old_minor] = old_line
727
    return results
728

    
729
  @classmethod
730
  def _GetVersion(cls):
731
    """Return the DRBD version.
732

733
    This will return a dict with keys:
734
      - k_major
735
      - k_minor
736
      - k_point
737
      - api
738
      - proto
739
      - proto2 (only on drbd > 8.2.X)
740

741
    """
742
    proc_data = cls._GetProcData()
743
    first_line = proc_data[0].strip()
744
    version = cls._VERSION_RE.match(first_line)
745
    if not version:
746
      raise errors.BlockDeviceError("Can't parse DRBD version from '%s'" %
747
                                    first_line)
748

    
749
    values = version.groups()
750
    retval = {'k_major': int(values[0]),
751
              'k_minor': int(values[1]),
752
              'k_point': int(values[2]),
753
              'api': int(values[3]),
754
              'proto': int(values[4]),
755
             }
756
    if values[5] is not None:
757
      retval['proto2'] = values[5]
758

    
759
    return retval
760

    
761
  @staticmethod
762
  def _DevPath(minor):
763
    """Return the path to a drbd device for a given minor.
764

765
    """
766
    return "/dev/drbd%d" % minor
767

    
768
  @classmethod
769
  def GetUsedDevs(cls):
770
    """Compute the list of used DRBD devices.
771

772
    """
773
    data = cls._GetProcData()
774

    
775
    used_devs = {}
776
    valid_line = re.compile("^ *([0-9]+): cs:([^ ]+).*$")
777
    for line in data:
778
      match = valid_line.match(line)
779
      if not match:
780
        continue
781
      minor = int(match.group(1))
782
      state = match.group(2)
783
      if state == cls._ST_UNCONFIGURED:
784
        continue
785
      used_devs[minor] = state, line
786

    
787
    return used_devs
788

    
789
  def _SetFromMinor(self, minor):
790
    """Set our parameters based on the given minor.
791

792
    This sets our minor variable and our dev_path.
793

794
    """
795
    if minor is None:
796
      self.minor = self.dev_path = None
797
      self.attached = False
798
    else:
799
      self.minor = minor
800
      self.dev_path = self._DevPath(minor)
801
      self.attached = True
802

    
803
  @staticmethod
804
  def _CheckMetaSize(meta_device):
805
    """Check if the given meta device looks like a valid one.
806

807
    This currently only check the size, which must be around
808
    128MiB.
809

810
    """
811
    result = utils.RunCmd(["blockdev", "--getsize", meta_device])
812
    if result.failed:
813
      _ThrowError("Failed to get device size: %s - %s",
814
                  result.fail_reason, result.output)
815
    try:
816
      sectors = int(result.stdout)
817
    except ValueError:
818
      _ThrowError("Invalid output from blockdev: '%s'", result.stdout)
819
    bytes = sectors * 512
820
    if bytes < 128 * 1024 * 1024: # less than 128MiB
821
      _ThrowError("Meta device too small (%.2fMib)", (bytes / 1024 / 1024))
822
    if bytes > (128 + 32) * 1024 * 1024: # account for an extra (big) PE on LVM
823
      _ThrowError("Meta device too big (%.2fMiB)", (bytes / 1024 / 1024))
824

    
825
  def Rename(self, new_id):
826
    """Rename a device.
827

828
    This is not supported for drbd devices.
829

830
    """
831
    raise errors.ProgrammerError("Can't rename a drbd device")
832

    
833

    
834
class DRBD8(BaseDRBD):
835
  """DRBD v8.x block device.
836

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

841
  The unique_id for the drbd device is the (local_ip, local_port,
842
  remote_ip, remote_port) tuple, and it must have two children: the
843
  data device and the meta_device. The meta device is checked for
844
  valid size and is zeroed on create.
845

846
  """
847
  _MAX_MINORS = 255
848
  _PARSE_SHOW = None
849

    
850
  # timeout constants
851
  _NET_RECONFIG_TIMEOUT = 60
852

    
853
  def __init__(self, unique_id, children, size):
854
    if children and children.count(None) > 0:
855
      children = []
856
    super(DRBD8, self).__init__(unique_id, children, size)
857
    self.major = self._DRBD_MAJOR
858
    version = self._GetVersion()
859
    if version['k_major'] != 8 :
860
      _ThrowError("Mismatch in DRBD kernel version and requested ganeti"
861
                  " usage: kernel is %s.%s, ganeti wants 8.x",
862
                  version['k_major'], version['k_minor'])
863

    
864
    if len(children) not in (0, 2):
865
      raise ValueError("Invalid configuration data %s" % str(children))
866
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 6:
867
      raise ValueError("Invalid configuration data %s" % str(unique_id))
868
    (self._lhost, self._lport,
869
     self._rhost, self._rport,
870
     self._aminor, self._secret) = unique_id
871
    if (self._lhost is not None and self._lhost == self._rhost and
872
        self._lport == self._rport):
873
      raise ValueError("Invalid configuration data, same local/remote %s" %
874
                       (unique_id,))
875
    self.Attach()
876

    
877
  @classmethod
878
  def _InitMeta(cls, minor, dev_path):
879
    """Initialize a meta device.
880

881
    This will not work if the given minor is in use.
882

883
    """
884
    result = utils.RunCmd(["drbdmeta", "--force", cls._DevPath(minor),
885
                           "v08", dev_path, "0", "create-md"])
886
    if result.failed:
887
      _ThrowError("Can't initialize meta device: %s", result.output)
888

    
889
  @classmethod
890
  def _FindUnusedMinor(cls):
891
    """Find an unused DRBD device.
892

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

896
    """
897
    data = cls._GetProcData()
898

    
899
    unused_line = re.compile("^ *([0-9]+): cs:Unconfigured$")
900
    used_line = re.compile("^ *([0-9]+): cs:")
901
    highest = None
902
    for line in data:
903
      match = unused_line.match(line)
904
      if match:
905
        return int(match.group(1))
906
      match = used_line.match(line)
907
      if match:
908
        minor = int(match.group(1))
909
        highest = max(highest, minor)
910
    if highest is None: # there are no minors in use at all
911
      return 0
912
    if highest >= cls._MAX_MINORS:
913
      logging.error("Error: no free drbd minors!")
914
      raise errors.BlockDeviceError("Can't find a free DRBD minor")
915
    return highest + 1
916

    
917
  @classmethod
918
  def _GetShowParser(cls):
919
    """Return a parser for `drbd show` output.
920

921
    This will either create or return an already-create parser for the
922
    output of the command `drbd show`.
923

924
    """
925
    if cls._PARSE_SHOW is not None:
926
      return cls._PARSE_SHOW
927

    
928
    # pyparsing setup
929
    lbrace = pyp.Literal("{").suppress()
930
    rbrace = pyp.Literal("}").suppress()
931
    semi = pyp.Literal(";").suppress()
932
    # this also converts the value to an int
933
    number = pyp.Word(pyp.nums).setParseAction(lambda s, l, t: int(t[0]))
934

    
935
    comment = pyp.Literal ("#") + pyp.Optional(pyp.restOfLine)
936
    defa = pyp.Literal("_is_default").suppress()
937
    dbl_quote = pyp.Literal('"').suppress()
938

    
939
    keyword = pyp.Word(pyp.alphanums + '-')
940

    
941
    # value types
942
    value = pyp.Word(pyp.alphanums + '_-/.:')
943
    quoted = dbl_quote + pyp.CharsNotIn('"') + dbl_quote
944
    addr_type = (pyp.Optional(pyp.Literal("ipv4")).suppress() +
945
                 pyp.Optional(pyp.Literal("ipv6")).suppress())
946
    addr_port = (addr_type + pyp.Word(pyp.nums + '.') +
947
                 pyp.Literal(':').suppress() + number)
948
    # meta device, extended syntax
949
    meta_value = ((value ^ quoted) + pyp.Literal('[').suppress() +
950
                  number + pyp.Word(']').suppress())
951
    # device name, extended syntax
952
    device_value = pyp.Literal("minor").suppress() + number
953

    
954
    # a statement
955
    stmt = (~rbrace + keyword + ~lbrace +
956
            pyp.Optional(addr_port ^ value ^ quoted ^ meta_value ^
957
                         device_value) +
958
            pyp.Optional(defa) + semi +
959
            pyp.Optional(pyp.restOfLine).suppress())
960

    
961
    # an entire section
962
    section_name = pyp.Word(pyp.alphas + '_')
963
    section = section_name + lbrace + pyp.ZeroOrMore(pyp.Group(stmt)) + rbrace
964

    
965
    bnf = pyp.ZeroOrMore(pyp.Group(section ^ stmt))
966
    bnf.ignore(comment)
967

    
968
    cls._PARSE_SHOW = bnf
969

    
970
    return bnf
971

    
972
  @classmethod
973
  def _GetShowData(cls, minor):
974
    """Return the `drbdsetup show` data for a minor.
975

976
    """
977
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "show"])
978
    if result.failed:
979
      logging.error("Can't display the drbd config: %s - %s",
980
                    result.fail_reason, result.output)
981
      return None
982
    return result.stdout
983

    
984
  @classmethod
985
  def _GetDevInfo(cls, out):
986
    """Parse details about a given DRBD minor.
987

988
    This return, if available, the local backing device (as a path)
989
    and the local and remote (ip, port) information from a string
990
    containing the output of the `drbdsetup show` command as returned
991
    by _GetShowData.
992

993
    """
994
    data = {}
995
    if not out:
996
      return data
997

    
998
    bnf = cls._GetShowParser()
999
    # run pyparse
1000

    
1001
    try:
1002
      results = bnf.parseString(out)
1003
    except pyp.ParseException, err:
1004
      _ThrowError("Can't parse drbdsetup show output: %s", str(err))
1005

    
1006
    # and massage the results into our desired format
1007
    for section in results:
1008
      sname = section[0]
1009
      if sname == "_this_host":
1010
        for lst in section[1:]:
1011
          if lst[0] == "disk":
1012
            data["local_dev"] = lst[1]
1013
          elif lst[0] == "meta-disk":
1014
            data["meta_dev"] = lst[1]
1015
            data["meta_index"] = lst[2]
1016
          elif lst[0] == "address":
1017
            data["local_addr"] = tuple(lst[1:])
1018
      elif sname == "_remote_host":
1019
        for lst in section[1:]:
1020
          if lst[0] == "address":
1021
            data["remote_addr"] = tuple(lst[1:])
1022
    return data
1023

    
1024
  def _MatchesLocal(self, info):
1025
    """Test if our local config matches with an existing device.
1026

1027
    The parameter should be as returned from `_GetDevInfo()`. This
1028
    method tests if our local backing device is the same as the one in
1029
    the info parameter, in effect testing if we look like the given
1030
    device.
1031

1032
    """
1033
    if self._children:
1034
      backend, meta = self._children
1035
    else:
1036
      backend = meta = None
1037

    
1038
    if backend is not None:
1039
      retval = ("local_dev" in info and info["local_dev"] == backend.dev_path)
1040
    else:
1041
      retval = ("local_dev" not in info)
1042

    
1043
    if meta is not None:
1044
      retval = retval and ("meta_dev" in info and
1045
                           info["meta_dev"] == meta.dev_path)
1046
      retval = retval and ("meta_index" in info and
1047
                           info["meta_index"] == 0)
1048
    else:
1049
      retval = retval and ("meta_dev" not in info and
1050
                           "meta_index" not in info)
1051
    return retval
1052

    
1053
  def _MatchesNet(self, info):
1054
    """Test if our network config matches with an existing device.
1055

1056
    The parameter should be as returned from `_GetDevInfo()`. This
1057
    method tests if our network configuration is the same as the one
1058
    in the info parameter, in effect testing if we look like the given
1059
    device.
1060

1061
    """
1062
    if (((self._lhost is None and not ("local_addr" in info)) and
1063
         (self._rhost is None and not ("remote_addr" in info)))):
1064
      return True
1065

    
1066
    if self._lhost is None:
1067
      return False
1068

    
1069
    if not ("local_addr" in info and
1070
            "remote_addr" in info):
1071
      return False
1072

    
1073
    retval = (info["local_addr"] == (self._lhost, self._lport))
1074
    retval = (retval and
1075
              info["remote_addr"] == (self._rhost, self._rport))
1076
    return retval
1077

    
1078
  @classmethod
1079
  def _AssembleLocal(cls, minor, backend, meta, size):
1080
    """Configure the local part of a DRBD device.
1081

1082
    """
1083
    args = ["drbdsetup", cls._DevPath(minor), "disk",
1084
            backend, meta, "0",
1085
            "-d", "%sm" % size,
1086
            "-e", "detach",
1087
            "--create-device"]
1088
    result = utils.RunCmd(args)
1089
    if result.failed:
1090
      _ThrowError("drbd%d: can't attach local disk: %s", minor, result.output)
1091

    
1092
  @classmethod
1093
  def _AssembleNet(cls, minor, net_info, protocol,
1094
                   dual_pri=False, hmac=None, secret=None):
1095
    """Configure the network part of the device.
1096

1097
    """
1098
    lhost, lport, rhost, rport = net_info
1099
    if None in net_info:
1100
      # we don't want network connection and actually want to make
1101
      # sure its shutdown
1102
      cls._ShutdownNet(minor)
1103
      return
1104

    
1105
    # Workaround for a race condition. When DRBD is doing its dance to
1106
    # establish a connection with its peer, it also sends the
1107
    # synchronization speed over the wire. In some cases setting the
1108
    # sync speed only after setting up both sides can race with DRBD
1109
    # connecting, hence we set it here before telling DRBD anything
1110
    # about its peer.
1111
    cls._SetMinorSyncSpeed(minor, constants.SYNC_SPEED)
1112

    
1113
    args = ["drbdsetup", cls._DevPath(minor), "net",
1114
            "%s:%s" % (lhost, lport), "%s:%s" % (rhost, rport), protocol,
1115
            "-A", "discard-zero-changes",
1116
            "-B", "consensus",
1117
            "--create-device",
1118
            ]
1119
    if dual_pri:
1120
      args.append("-m")
1121
    if hmac and secret:
1122
      args.extend(["-a", hmac, "-x", secret])
1123
    result = utils.RunCmd(args)
1124
    if result.failed:
1125
      _ThrowError("drbd%d: can't setup network: %s - %s",
1126
                  minor, result.fail_reason, result.output)
1127

    
1128
    timeout = time.time() + 10
1129
    ok = False
1130
    while time.time() < timeout:
1131
      info = cls._GetDevInfo(cls._GetShowData(minor))
1132
      if not "local_addr" in info or not "remote_addr" in info:
1133
        time.sleep(1)
1134
        continue
1135
      if (info["local_addr"] != (lhost, lport) or
1136
          info["remote_addr"] != (rhost, rport)):
1137
        time.sleep(1)
1138
        continue
1139
      ok = True
1140
      break
1141
    if not ok:
1142
      _ThrowError("drbd%d: timeout while configuring network", minor)
1143

    
1144
  def AddChildren(self, devices):
1145
    """Add a disk to the DRBD device.
1146

1147
    """
1148
    if self.minor is None:
1149
      _ThrowError("drbd%d: can't attach to dbrd8 during AddChildren",
1150
                  self._aminor)
1151
    if len(devices) != 2:
1152
      _ThrowError("drbd%d: need two devices for AddChildren", self.minor)
1153
    info = self._GetDevInfo(self._GetShowData(self.minor))
1154
    if "local_dev" in info:
1155
      _ThrowError("drbd%d: already attached to a local disk", self.minor)
1156
    backend, meta = devices
1157
    if backend.dev_path is None or meta.dev_path is None:
1158
      _ThrowError("drbd%d: children not ready during AddChildren", self.minor)
1159
    backend.Open()
1160
    meta.Open()
1161
    self._CheckMetaSize(meta.dev_path)
1162
    self._InitMeta(self._FindUnusedMinor(), meta.dev_path)
1163

    
1164
    self._AssembleLocal(self.minor, backend.dev_path, meta.dev_path, self.size)
1165
    self._children = devices
1166

    
1167
  def RemoveChildren(self, devices):
1168
    """Detach the drbd device from local storage.
1169

1170
    """
1171
    if self.minor is None:
1172
      _ThrowError("drbd%d: can't attach to drbd8 during RemoveChildren",
1173
                  self._aminor)
1174
    # early return if we don't actually have backing storage
1175
    info = self._GetDevInfo(self._GetShowData(self.minor))
1176
    if "local_dev" not in info:
1177
      return
1178
    if len(self._children) != 2:
1179
      _ThrowError("drbd%d: we don't have two children: %s", self.minor,
1180
                  self._children)
1181
    if self._children.count(None) == 2: # we don't actually have children :)
1182
      logging.warning("drbd%d: requested detach while detached", self.minor)
1183
      return
1184
    if len(devices) != 2:
1185
      _ThrowError("drbd%d: we need two children in RemoveChildren", self.minor)
1186
    for child, dev in zip(self._children, devices):
1187
      if dev != child.dev_path:
1188
        _ThrowError("drbd%d: mismatch in local storage (%s != %s) in"
1189
                    " RemoveChildren", self.minor, dev, child.dev_path)
1190

    
1191
    self._ShutdownLocal(self.minor)
1192
    self._children = []
1193

    
1194
  @classmethod
1195
  def _SetMinorSyncSpeed(cls, minor, kbytes):
1196
    """Set the speed of the DRBD syncer.
1197

1198
    This is the low-level implementation.
1199

1200
    @type minor: int
1201
    @param minor: the drbd minor whose settings we change
1202
    @type kbytes: int
1203
    @param kbytes: the speed in kbytes/second
1204
    @rtype: boolean
1205
    @return: the success of the operation
1206

1207
    """
1208
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "syncer",
1209
                           "-r", "%d" % kbytes, "--create-device"])
1210
    if result.failed:
1211
      logging.error("Can't change syncer rate: %s - %s",
1212
                    result.fail_reason, result.output)
1213
    return not result.failed
1214

    
1215
  def SetSyncSpeed(self, kbytes):
1216
    """Set the speed of the DRBD syncer.
1217

1218
    @type kbytes: int
1219
    @param kbytes: the speed in kbytes/second
1220
    @rtype: boolean
1221
    @return: the success of the operation
1222

1223
    """
1224
    if self.minor is None:
1225
      logging.info("Not attached during SetSyncSpeed")
1226
      return False
1227
    children_result = super(DRBD8, self).SetSyncSpeed(kbytes)
1228
    return self._SetMinorSyncSpeed(self.minor, kbytes) and children_result
1229

    
1230
  def GetProcStatus(self):
1231
    """Return device data from /proc.
1232

1233
    """
1234
    if self.minor is None:
1235
      _ThrowError("drbd%d: GetStats() called while not attached", self._aminor)
1236
    proc_info = self._MassageProcData(self._GetProcData())
1237
    if self.minor not in proc_info:
1238
      _ThrowError("drbd%d: can't find myself in /proc", self.minor)
1239
    return DRBD8Status(proc_info[self.minor])
1240

    
1241
  def GetSyncStatus(self):
1242
    """Returns the sync status of the device.
1243

1244

1245
    If sync_percent is None, it means all is ok
1246
    If estimated_time is None, it means we can't esimate
1247
    the time needed, otherwise it's the time left in seconds.
1248

1249

1250
    We set the is_degraded parameter to True on two conditions:
1251
    network not connected or local disk missing.
1252

1253
    We compute the ldisk parameter based on wheter we have a local
1254
    disk or not.
1255

1256
    @rtype: tuple
1257
    @return: (sync_percent, estimated_time, is_degraded, ldisk)
1258

1259
    """
1260
    if self.minor is None and not self.Attach():
1261
      _ThrowError("drbd%d: can't Attach() in GetSyncStatus", self._aminor)
1262
    stats = self.GetProcStatus()
1263
    ldisk = not stats.is_disk_uptodate
1264
    is_degraded = not stats.is_connected
1265
    return stats.sync_percent, stats.est_time, is_degraded or ldisk, ldisk
1266

    
1267
  def Open(self, force=False):
1268
    """Make the local state primary.
1269

1270
    If the 'force' parameter is given, the '-o' option is passed to
1271
    drbdsetup. Since this is a potentially dangerous operation, the
1272
    force flag should be only given after creation, when it actually
1273
    is mandatory.
1274

1275
    """
1276
    if self.minor is None and not self.Attach():
1277
      logging.error("DRBD cannot attach to a device during open")
1278
      return False
1279
    cmd = ["drbdsetup", self.dev_path, "primary"]
1280
    if force:
1281
      cmd.append("-o")
1282
    result = utils.RunCmd(cmd)
1283
    if result.failed:
1284
      _ThrowError("drbd%d: can't make drbd device primary: %s", self.minor,
1285
                  result.output)
1286

    
1287
  def Close(self):
1288
    """Make the local state secondary.
1289

1290
    This will, of course, fail if the device is in use.
1291

1292
    """
1293
    if self.minor is None and not self.Attach():
1294
      _ThrowError("drbd%d: can't Attach() in Close()", self._aminor)
1295
    result = utils.RunCmd(["drbdsetup", self.dev_path, "secondary"])
1296
    if result.failed:
1297
      _ThrowError("drbd%d: can't switch drbd device to secondary: %s",
1298
                  self.minor, result.output)
1299

    
1300
  def DisconnectNet(self):
1301
    """Removes network configuration.
1302

1303
    This method shutdowns the network side of the device.
1304

1305
    The method will wait up to a hardcoded timeout for the device to
1306
    go into standalone after the 'disconnect' command before
1307
    re-configuring it, as sometimes it takes a while for the
1308
    disconnect to actually propagate and thus we might issue a 'net'
1309
    command while the device is still connected. If the device will
1310
    still be attached to the network and we time out, we raise an
1311
    exception.
1312

1313
    """
1314
    if self.minor is None:
1315
      _ThrowError("drbd%d: disk not attached in re-attach net", self._aminor)
1316

    
1317
    if None in (self._lhost, self._lport, self._rhost, self._rport):
1318
      _ThrowError("drbd%d: DRBD disk missing network info in"
1319
                  " DisconnectNet()", self.minor)
1320

    
1321
    ever_disconnected = _IgnoreError(self._ShutdownNet, self.minor)
1322
    timeout_limit = time.time() + self._NET_RECONFIG_TIMEOUT
1323
    sleep_time = 0.100 # we start the retry time at 100 miliseconds
1324
    while time.time() < timeout_limit:
1325
      status = self.GetProcStatus()
1326
      if status.is_standalone:
1327
        break
1328
      # retry the disconnect, it seems possible that due to a
1329
      # well-time disconnect on the peer, my disconnect command might
1330
      # be ingored and forgotten
1331
      ever_disconnected = _IgnoreError(self._ShutdownNet, self.minor) or \
1332
                          ever_disconnected
1333
      time.sleep(sleep_time)
1334
      sleep_time = min(2, sleep_time * 1.5)
1335

    
1336
    if not status.is_standalone:
1337
      if ever_disconnected:
1338
        msg = ("drbd%d: device did not react to the"
1339
               " 'disconnect' command in a timely manner")
1340
      else:
1341
        msg = "drbd%d: can't shutdown network, even after multiple retries"
1342
      _ThrowError(msg, self.minor)
1343

    
1344
    reconfig_time = time.time() - timeout_limit + self._NET_RECONFIG_TIMEOUT
1345
    if reconfig_time > 15: # hardcoded alert limit
1346
      logging.info("drbd%d: DisconnectNet: detach took %.3f seconds",
1347
                   self.minor, reconfig_time)
1348

    
1349
  def AttachNet(self, multimaster):
1350
    """Reconnects the network.
1351

1352
    This method connects the network side of the device with a
1353
    specified multi-master flag. The device needs to be 'Standalone'
1354
    but have valid network configuration data.
1355

1356
    Args:
1357
      - multimaster: init the network in dual-primary mode
1358

1359
    """
1360
    if self.minor is None:
1361
      _ThrowError("drbd%d: device not attached in AttachNet", self._aminor)
1362

    
1363
    if None in (self._lhost, self._lport, self._rhost, self._rport):
1364
      _ThrowError("drbd%d: missing network info in AttachNet()", self.minor)
1365

    
1366
    status = self.GetProcStatus()
1367

    
1368
    if not status.is_standalone:
1369
      _ThrowError("drbd%d: device is not standalone in AttachNet", self.minor)
1370

    
1371
    self._AssembleNet(self.minor,
1372
                      (self._lhost, self._lport, self._rhost, self._rport),
1373
                      constants.DRBD_NET_PROTOCOL, dual_pri=multimaster,
1374
                      hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1375

    
1376
  def Attach(self):
1377
    """Check if our minor is configured.
1378

1379
    This doesn't do any device configurations - it only checks if the
1380
    minor is in a state different from Unconfigured.
1381

1382
    Note that this function will not change the state of the system in
1383
    any way (except in case of side-effects caused by reading from
1384
    /proc).
1385

1386
    """
1387
    used_devs = self.GetUsedDevs()
1388
    if self._aminor in used_devs:
1389
      minor = self._aminor
1390
    else:
1391
      minor = None
1392

    
1393
    self._SetFromMinor(minor)
1394
    return minor is not None
1395

    
1396
  def Assemble(self):
1397
    """Assemble the drbd.
1398

1399
    Method:
1400
      - if we have a configured device, we try to ensure that it matches
1401
        our config
1402
      - if not, we create it from zero
1403

1404
    """
1405
    super(DRBD8, self).Assemble()
1406

    
1407
    self.Attach()
1408
    if self.minor is None:
1409
      # local device completely unconfigured
1410
      self._FastAssemble()
1411
    else:
1412
      # we have to recheck the local and network status and try to fix
1413
      # the device
1414
      self._SlowAssemble()
1415

    
1416
  def _SlowAssemble(self):
1417
    """Assembles the DRBD device from a (partially) configured device.
1418

1419
    In case of partially attached (local device matches but no network
1420
    setup), we perform the network attach. If successful, we re-test
1421
    the attach if can return success.
1422

1423
    """
1424
    net_data = (self._lhost, self._lport, self._rhost, self._rport)
1425
    for minor in (self._aminor,):
1426
      info = self._GetDevInfo(self._GetShowData(minor))
1427
      match_l = self._MatchesLocal(info)
1428
      match_r = self._MatchesNet(info)
1429

    
1430
      if match_l and match_r:
1431
        # everything matches
1432
        break
1433

    
1434
      if match_l and not match_r and "local_addr" not in info:
1435
        # disk matches, but not attached to network, attach and recheck
1436
        self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
1437
                          hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1438
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1439
          break
1440
        else:
1441
          _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
1442
                      " show' disagrees", minor)
1443

    
1444
      if match_r and "local_dev" not in info:
1445
        # no local disk, but network attached and it matches
1446
        self._AssembleLocal(minor, self._children[0].dev_path,
1447
                            self._children[1].dev_path, self.size)
1448
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1449
          break
1450
        else:
1451
          _ThrowError("drbd%d: disk attach successful, but 'drbdsetup"
1452
                      " show' disagrees", minor)
1453

    
1454
      # this case must be considered only if we actually have local
1455
      # storage, i.e. not in diskless mode, because all diskless
1456
      # devices are equal from the point of view of local
1457
      # configuration
1458
      if (match_l and "local_dev" in info and
1459
          not match_r and "local_addr" in info):
1460
        # strange case - the device network part points to somewhere
1461
        # else, even though its local storage is ours; as we own the
1462
        # drbd space, we try to disconnect from the remote peer and
1463
        # reconnect to our correct one
1464
        try:
1465
          self._ShutdownNet(minor)
1466
        except errors.BlockDeviceError, err:
1467
          _ThrowError("drbd%d: device has correct local storage, wrong"
1468
                      " remote peer and is unable to disconnect in order"
1469
                      " to attach to the correct peer: %s", minor, str(err))
1470
        # note: _AssembleNet also handles the case when we don't want
1471
        # local storage (i.e. one or more of the _[lr](host|port) is
1472
        # None)
1473
        self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
1474
                          hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1475
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1476
          break
1477
        else:
1478
          _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
1479
                      " show' disagrees", minor)
1480

    
1481
    else:
1482
      minor = None
1483

    
1484
    self._SetFromMinor(minor)
1485
    if minor is None:
1486
      _ThrowError("drbd%d: cannot activate, unknown or unhandled reason",
1487
                  self._aminor)
1488

    
1489
  def _FastAssemble(self):
1490
    """Assemble the drbd device from zero.
1491

1492
    This is run when in Assemble we detect our minor is unused.
1493

1494
    """
1495
    minor = self._aminor
1496
    if self._children and self._children[0] and self._children[1]:
1497
      self._AssembleLocal(minor, self._children[0].dev_path,
1498
                          self._children[1].dev_path, self.size)
1499
    if self._lhost and self._lport and self._rhost and self._rport:
1500
      self._AssembleNet(minor,
1501
                        (self._lhost, self._lport, self._rhost, self._rport),
1502
                        constants.DRBD_NET_PROTOCOL,
1503
                        hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1504
    self._SetFromMinor(minor)
1505

    
1506
  @classmethod
1507
  def _ShutdownLocal(cls, minor):
1508
    """Detach from the local device.
1509

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

1513
    """
1514
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "detach"])
1515
    if result.failed:
1516
      _ThrowError("drbd%d: can't detach local disk: %s", minor, result.output)
1517

    
1518
  @classmethod
1519
  def _ShutdownNet(cls, minor):
1520
    """Disconnect from the remote peer.
1521

1522
    This fails if we don't have a local device.
1523

1524
    """
1525
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disconnect"])
1526
    if result.failed:
1527
      _ThrowError("drbd%d: can't shutdown network: %s", minor, result.output)
1528

    
1529
  @classmethod
1530
  def _ShutdownAll(cls, minor):
1531
    """Deactivate the device.
1532

1533
    This will, of course, fail if the device is in use.
1534

1535
    """
1536
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "down"])
1537
    if result.failed:
1538
      _ThrowError("drbd%d: can't shutdown drbd device: %s",
1539
                  minor, result.output)
1540

    
1541
  def Shutdown(self):
1542
    """Shutdown the DRBD device.
1543

1544
    """
1545
    if self.minor is None and not self.Attach():
1546
      logging.info("drbd%d: not attached during Shutdown()", self._aminor)
1547
      return
1548
    minor = self.minor
1549
    self.minor = None
1550
    self.dev_path = None
1551
    self._ShutdownAll(minor)
1552

    
1553
  def Remove(self):
1554
    """Stub remove for DRBD devices.
1555

1556
    """
1557
    self.Shutdown()
1558

    
1559
  @classmethod
1560
  def Create(cls, unique_id, children, size):
1561
    """Create a new DRBD8 device.
1562

1563
    Since DRBD devices are not created per se, just assembled, this
1564
    function only initializes the metadata.
1565

1566
    """
1567
    if len(children) != 2:
1568
      raise errors.ProgrammerError("Invalid setup for the drbd device")
1569
    # check that the minor is unused
1570
    aminor = unique_id[4]
1571
    proc_info = cls._MassageProcData(cls._GetProcData())
1572
    if aminor in proc_info:
1573
      status = DRBD8Status(proc_info[aminor])
1574
      in_use = status.is_in_use
1575
    else:
1576
      in_use = False
1577
    if in_use:
1578
      _ThrowError("drbd%d: minor is already in use at Create() time", aminor)
1579
    meta = children[1]
1580
    meta.Assemble()
1581
    if not meta.Attach():
1582
      _ThrowError("drbd%d: can't attach to meta device '%s'",
1583
                  aminor, meta)
1584
    cls._CheckMetaSize(meta.dev_path)
1585
    cls._InitMeta(aminor, meta.dev_path)
1586
    return cls(unique_id, children, size)
1587

    
1588
  def Grow(self, amount):
1589
    """Resize the DRBD device and its backing storage.
1590

1591
    """
1592
    if self.minor is None:
1593
      _ThrowError("drbd%d: Grow called while not attached", self._aminor)
1594
    if len(self._children) != 2 or None in self._children:
1595
      _ThrowError("drbd%d: cannot grow diskless device", self.minor)
1596
    self._children[0].Grow(amount)
1597
    result = utils.RunCmd(["drbdsetup", self.dev_path, "resize"])
1598
    if result.failed:
1599
      _ThrowError("drbd%d: resize failed: %s", self.minor, result.output)
1600

    
1601

    
1602
class FileStorage(BlockDev):
1603
  """File device.
1604

1605
  This class represents the a file storage backend device.
1606

1607
  The unique_id for the file device is a (file_driver, file_path) tuple.
1608

1609
  """
1610
  def __init__(self, unique_id, children, size):
1611
    """Initalizes a file device backend.
1612

1613
    """
1614
    if children:
1615
      raise errors.BlockDeviceError("Invalid setup for file device")
1616
    super(FileStorage, self).__init__(unique_id, children, size)
1617
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1618
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1619
    self.driver = unique_id[0]
1620
    self.dev_path = unique_id[1]
1621
    self.Attach()
1622

    
1623
  def Assemble(self):
1624
    """Assemble the device.
1625

1626
    Checks whether the file device exists, raises BlockDeviceError otherwise.
1627

1628
    """
1629
    if not os.path.exists(self.dev_path):
1630
      _ThrowError("File device '%s' does not exist" % self.dev_path)
1631

    
1632
  def Shutdown(self):
1633
    """Shutdown the device.
1634

1635
    This is a no-op for the file type, as we don't deacivate
1636
    the file on shutdown.
1637

1638
    """
1639
    pass
1640

    
1641
  def Open(self, force=False):
1642
    """Make the device ready for I/O.
1643

1644
    This is a no-op for the file type.
1645

1646
    """
1647
    pass
1648

    
1649
  def Close(self):
1650
    """Notifies that the device will no longer be used for I/O.
1651

1652
    This is a no-op for the file type.
1653

1654
    """
1655
    pass
1656

    
1657
  def Remove(self):
1658
    """Remove the file backing the block device.
1659

1660
    @rtype: boolean
1661
    @return: True if the removal was successful
1662

1663
    """
1664
    try:
1665
      os.remove(self.dev_path)
1666
    except OSError, err:
1667
      if err.errno != errno.ENOENT:
1668
        _ThrowError("Can't remove file '%s': %s", self.dev_path, err)
1669

    
1670
  def Attach(self):
1671
    """Attach to an existing file.
1672

1673
    Check if this file already exists.
1674

1675
    @rtype: boolean
1676
    @return: True if file exists
1677

1678
    """
1679
    self.attached = os.path.exists(self.dev_path)
1680
    return self.attached
1681

    
1682
  @classmethod
1683
  def Create(cls, unique_id, children, size):
1684
    """Create a new file.
1685

1686
    @param size: the size of file in MiB
1687

1688
    @rtype: L{bdev.FileStorage}
1689
    @return: an instance of FileStorage
1690

1691
    """
1692
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1693
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1694
    dev_path = unique_id[1]
1695
    if os.path.exists(dev_path):
1696
      _ThrowError("File already existing: %s", dev_path)
1697
    try:
1698
      f = open(dev_path, 'w')
1699
      f.truncate(size * 1024 * 1024)
1700
      f.close()
1701
    except IOError, err:
1702
      _ThrowError("Error in file creation: %", str(err))
1703

    
1704
    return FileStorage(unique_id, children, size)
1705

    
1706

    
1707
DEV_MAP = {
1708
  constants.LD_LV: LogicalVolume,
1709
  constants.LD_DRBD8: DRBD8,
1710
  constants.LD_FILE: FileStorage,
1711
  }
1712

    
1713

    
1714
def FindDevice(dev_type, unique_id, children, size):
1715
  """Search for an existing, assembled device.
1716

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

1720
  """
1721
  if dev_type not in DEV_MAP:
1722
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1723
  device = DEV_MAP[dev_type](unique_id, children, size)
1724
  if not device.attached:
1725
    return None
1726
  return device
1727

    
1728

    
1729
def Assemble(dev_type, unique_id, children, size):
1730
  """Try to attach or assemble an existing device.
1731

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

1735
  """
1736
  if dev_type not in DEV_MAP:
1737
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1738
  device = DEV_MAP[dev_type](unique_id, children, size)
1739
  device.Assemble()
1740
  return device
1741

    
1742

    
1743
def Create(dev_type, unique_id, children, size):
1744
  """Create a device.
1745

1746
  """
1747
  if dev_type not in DEV_MAP:
1748
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1749
  device = DEV_MAP[dev_type].Create(unique_id, children, size)
1750
  return device