Statistics
| Branch: | Tag: | Revision:

root / lib / bdev.py @ f2f57b6e

History | View | Annotate | Download (76.7 kB)

1
#
2
#
3

    
4
# Copyright (C) 2006, 2007, 2010, 2011 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 shlex
28
import stat
29
import pyparsing as pyp
30
import os
31
import logging
32

    
33
from ganeti import utils
34
from ganeti import errors
35
from ganeti import constants
36
from ganeti import objects
37
from ganeti import compat
38
from ganeti import netutils
39

    
40

    
41
# Size of reads in _CanReadDevice
42
_DEVICE_READ_SIZE = 128 * 1024
43

    
44

    
45
def _IgnoreError(fn, *args, **kwargs):
46
  """Executes the given function, ignoring BlockDeviceErrors.
47

48
  This is used in order to simplify the execution of cleanup or
49
  rollback functions.
50

51
  @rtype: boolean
52
  @return: True when fn didn't raise an exception, False otherwise
53

54
  """
55
  try:
56
    fn(*args, **kwargs)
57
    return True
58
  except errors.BlockDeviceError, err:
59
    logging.warning("Caught BlockDeviceError but ignoring: %s", str(err))
60
    return False
61

    
62

    
63
def _ThrowError(msg, *args):
64
  """Log an error to the node daemon and the raise an exception.
65

66
  @type msg: string
67
  @param msg: the text of the exception
68
  @raise errors.BlockDeviceError
69

70
  """
71
  if args:
72
    msg = msg % args
73
  logging.error(msg)
74
  raise errors.BlockDeviceError(msg)
75

    
76

    
77
def _CanReadDevice(path):
78
  """Check if we can read from the given device.
79

80
  This tries to read the first 128k of the device.
81

82
  """
83
  try:
84
    utils.ReadFile(path, size=_DEVICE_READ_SIZE)
85
    return True
86
  except EnvironmentError:
87
    logging.warning("Can't read from device %s", path, exc_info=True)
88
    return False
89

    
90

    
91
class BlockDev(object):
92
  """Block device abstract class.
93

94
  A block device can be in the following states:
95
    - not existing on the system, and by `Create()` it goes into:
96
    - existing but not setup/not active, and by `Assemble()` goes into:
97
    - active read-write and by `Open()` it goes into
98
    - online (=used, or ready for use)
99

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

106
  The many different states of the device are due to the fact that we
107
  need to cover many device types:
108
    - logical volumes are created, lvchange -a y $lv, and used
109
    - drbd devices are attached to a local disk/remote peer and made primary
110

111
  A block device is identified by three items:
112
    - the /dev path of the device (dynamic)
113
    - a unique ID of the device (static)
114
    - it's major/minor pair (dynamic)
115

116
  Not all devices implement both the first two as distinct items. LVM
117
  logical volumes have their unique ID (the pair volume group, logical
118
  volume name) in a 1-to-1 relation to the dev path. For DRBD devices,
119
  the /dev path is again dynamic and the unique id is the pair (host1,
120
  dev1), (host2, dev2).
121

122
  You can get to a device in two ways:
123
    - creating the (real) device, which returns you
124
      an attached instance (lvcreate)
125
    - attaching of a python instance to an existing (real) device
126

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

133
  """
134
  def __init__(self, unique_id, children, size, params):
135
    self._children = children
136
    self.dev_path = None
137
    self.unique_id = unique_id
138
    self.major = None
139
    self.minor = None
140
    self.attached = False
141
    self.size = size
142
    self.params = params
143

    
144
  def Assemble(self):
145
    """Assemble the device from its components.
146

147
    Implementations of this method by child classes must ensure that:
148
      - after the device has been assembled, it knows its major/minor
149
        numbers; this allows other devices (usually parents) to probe
150
        correctly for their children
151
      - calling this method on an existing, in-use device is safe
152
      - if the device is already configured (and in an OK state),
153
        this method is idempotent
154

155
    """
156
    pass
157

    
158
  def Attach(self):
159
    """Find a device which matches our config and attach to it.
160

161
    """
162
    raise NotImplementedError
163

    
164
  def Close(self):
165
    """Notifies that the device will no longer be used for I/O.
166

167
    """
168
    raise NotImplementedError
169

    
170
  @classmethod
171
  def Create(cls, unique_id, children, size, params):
172
    """Create the device.
173

174
    If the device cannot be created, it will return None
175
    instead. Error messages go to the logging system.
176

177
    Note that for some devices, the unique_id is used, and for other,
178
    the children. The idea is that these two, taken together, are
179
    enough for both creation and assembly (later).
180

181
    """
182
    raise NotImplementedError
183

    
184
  def Remove(self):
185
    """Remove this device.
186

187
    This makes sense only for some of the device types: LV and file
188
    storage. Also note that if the device can't attach, the removal
189
    can't be completed.
190

191
    """
192
    raise NotImplementedError
193

    
194
  def Rename(self, new_id):
195
    """Rename this device.
196

197
    This may or may not make sense for a given device type.
198

199
    """
200
    raise NotImplementedError
201

    
202
  def Open(self, force=False):
203
    """Make the device ready for use.
204

205
    This makes the device ready for I/O. For now, just the DRBD
206
    devices need this.
207

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

211
    """
212
    raise NotImplementedError
213

    
214
  def Shutdown(self):
215
    """Shut down the device, freeing its children.
216

217
    This undoes the `Assemble()` work, except for the child
218
    assembling; as such, the children on the device are still
219
    assembled after this call.
220

221
    """
222
    raise NotImplementedError
223

    
224
  def SetSyncParams(self, params):
225
    """Adjust the synchronization parameters of the mirror.
226

227
    In case this is not a mirroring device, this is no-op.
228

229
    @param params: dictionary of LD level disk parameters related to the
230
    synchronization.
231

232
    """
233
    result = True
234
    if self._children:
235
      for child in self._children:
236
        result = result and child.SetSyncParams(params)
237
    return result
238

    
239
  def PauseResumeSync(self, pause):
240
    """Pause/Resume the sync of the mirror.
241

242
    In case this is not a mirroring device, this is no-op.
243

244
    @param pause: Whether to pause or resume
245

246
    """
247
    result = True
248
    if self._children:
249
      for child in self._children:
250
        result = result and child.PauseResumeSync(pause)
251
    return result
252

    
253
  def GetSyncStatus(self):
254
    """Returns the sync status of the device.
255

256
    If this device is a mirroring device, this function returns the
257
    status of the mirror.
258

259
    If sync_percent is None, it means the device is not syncing.
260

261
    If estimated_time is None, it means we can't estimate
262
    the time needed, otherwise it's the time left in seconds.
263

264
    If is_degraded is True, it means the device is missing
265
    redundancy. This is usually a sign that something went wrong in
266
    the device setup, if sync_percent is None.
267

268
    The ldisk parameter represents the degradation of the local
269
    data. This is only valid for some devices, the rest will always
270
    return False (not degraded).
271

272
    @rtype: objects.BlockDevStatus
273

274
    """
275
    return objects.BlockDevStatus(dev_path=self.dev_path,
276
                                  major=self.major,
277
                                  minor=self.minor,
278
                                  sync_percent=None,
279
                                  estimated_time=None,
280
                                  is_degraded=False,
281
                                  ldisk_status=constants.LDS_OKAY)
282

    
283
  def CombinedSyncStatus(self):
284
    """Calculate the mirror status recursively for our children.
285

286
    The return value is the same as for `GetSyncStatus()` except the
287
    minimum percent and maximum time are calculated across our
288
    children.
289

290
    @rtype: objects.BlockDevStatus
291

292
    """
293
    status = self.GetSyncStatus()
294

    
295
    min_percent = status.sync_percent
296
    max_time = status.estimated_time
297
    is_degraded = status.is_degraded
298
    ldisk_status = status.ldisk_status
299

    
300
    if self._children:
301
      for child in self._children:
302
        child_status = child.GetSyncStatus()
303

    
304
        if min_percent is None:
305
          min_percent = child_status.sync_percent
306
        elif child_status.sync_percent is not None:
307
          min_percent = min(min_percent, child_status.sync_percent)
308

    
309
        if max_time is None:
310
          max_time = child_status.estimated_time
311
        elif child_status.estimated_time is not None:
312
          max_time = max(max_time, child_status.estimated_time)
313

    
314
        is_degraded = is_degraded or child_status.is_degraded
315

    
316
        if ldisk_status is None:
317
          ldisk_status = child_status.ldisk_status
318
        elif child_status.ldisk_status is not None:
319
          ldisk_status = max(ldisk_status, child_status.ldisk_status)
320

    
321
    return objects.BlockDevStatus(dev_path=self.dev_path,
322
                                  major=self.major,
323
                                  minor=self.minor,
324
                                  sync_percent=min_percent,
325
                                  estimated_time=max_time,
326
                                  is_degraded=is_degraded,
327
                                  ldisk_status=ldisk_status)
328

    
329
  def SetInfo(self, text):
330
    """Update metadata with info text.
331

332
    Only supported for some device types.
333

334
    """
335
    for child in self._children:
336
      child.SetInfo(text)
337

    
338
  def Grow(self, amount, dryrun):
339
    """Grow the block device.
340

341
    @type amount: integer
342
    @param amount: the amount (in mebibytes) to grow with
343
    @type dryrun: boolean
344
    @param dryrun: whether to execute the operation in simulation mode
345
        only, without actually increasing the size
346

347
    """
348
    raise NotImplementedError
349

    
350
  def GetActualSize(self):
351
    """Return the actual disk size.
352

353
    @note: the device needs to be active when this is called
354

355
    """
356
    assert self.attached, "BlockDevice not attached in GetActualSize()"
357
    result = utils.RunCmd(["blockdev", "--getsize64", self.dev_path])
358
    if result.failed:
359
      _ThrowError("blockdev failed (%s): %s",
360
                  result.fail_reason, result.output)
361
    try:
362
      sz = int(result.output.strip())
363
    except (ValueError, TypeError), err:
364
      _ThrowError("Failed to parse blockdev output: %s", str(err))
365
    return sz
366

    
367
  def __repr__(self):
368
    return ("<%s: unique_id: %s, children: %s, %s:%s, %s>" %
369
            (self.__class__, self.unique_id, self._children,
370
             self.major, self.minor, self.dev_path))
371

    
372

    
373
class LogicalVolume(BlockDev):
374
  """Logical Volume block device.
375

376
  """
377
  _VALID_NAME_RE = re.compile("^[a-zA-Z0-9+_.-]*$")
378
  _INVALID_NAMES = frozenset([".", "..", "snapshot", "pvmove"])
379
  _INVALID_SUBSTRINGS = frozenset(["_mlog", "_mimage"])
380

    
381
  def __init__(self, unique_id, children, size, params):
382
    """Attaches to a LV device.
383

384
    The unique_id is a tuple (vg_name, lv_name)
385

386
    """
387
    super(LogicalVolume, self).__init__(unique_id, children, size, params)
388
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
389
      raise ValueError("Invalid configuration data %s" % str(unique_id))
390
    self._vg_name, self._lv_name = unique_id
391
    self._ValidateName(self._vg_name)
392
    self._ValidateName(self._lv_name)
393
    self.dev_path = utils.PathJoin("/dev", self._vg_name, self._lv_name)
394
    self._degraded = True
395
    self.major = self.minor = self.pe_size = self.stripe_count = None
396
    self.Attach()
397

    
398
  @classmethod
399
  def Create(cls, unique_id, children, size, params):
400
    """Create a new logical volume.
401

402
    """
403
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
404
      raise errors.ProgrammerError("Invalid configuration data %s" %
405
                                   str(unique_id))
406
    vg_name, lv_name = unique_id
407
    cls._ValidateName(vg_name)
408
    cls._ValidateName(lv_name)
409
    pvs_info = cls.GetPVInfo([vg_name])
410
    if not pvs_info:
411
      _ThrowError("Can't compute PV info for vg %s", vg_name)
412
    pvs_info.sort()
413
    pvs_info.reverse()
414

    
415
    pvlist = [pv[1] for pv in pvs_info]
416
    if compat.any(":" in v for v in pvlist):
417
      _ThrowError("Some of your PVs have the invalid character ':' in their"
418
                  " name, this is not supported - please filter them out"
419
                  " in lvm.conf using either 'filter' or 'preferred_names'")
420
    free_size = sum([pv[0] for pv in pvs_info])
421
    current_pvs = len(pvlist)
422
    desired_stripes = params[constants.LDP_STRIPES]
423
    stripes = min(current_pvs, desired_stripes)
424
    if stripes < desired_stripes:
425
      logging.warning("Could not use %d stripes for VG %s, as only %d PVs are"
426
                      " available.", desired_stripes, vg_name, current_pvs)
427

    
428
    # The size constraint should have been checked from the master before
429
    # calling the create function.
430
    if free_size < size:
431
      _ThrowError("Not enough free space: required %s,"
432
                  " available %s", size, free_size)
433
    cmd = ["lvcreate", "-L%dm" % size, "-n%s" % lv_name]
434
    # If the free space is not well distributed, we won't be able to
435
    # create an optimally-striped volume; in that case, we want to try
436
    # with N, N-1, ..., 2, and finally 1 (non-stripped) number of
437
    # stripes
438
    for stripes_arg in range(stripes, 0, -1):
439
      result = utils.RunCmd(cmd + ["-i%d" % stripes_arg] + [vg_name] + pvlist)
440
      if not result.failed:
441
        break
442
    if result.failed:
443
      _ThrowError("LV create failed (%s): %s",
444
                  result.fail_reason, result.output)
445
    return LogicalVolume(unique_id, children, size, params)
446

    
447
  @staticmethod
448
  def _GetVolumeInfo(lvm_cmd, fields):
449
    """Returns LVM Volumen infos using lvm_cmd
450

451
    @param lvm_cmd: Should be one of "pvs", "vgs" or "lvs"
452
    @param fields: Fields to return
453
    @return: A list of dicts each with the parsed fields
454

455
    """
456
    if not fields:
457
      raise errors.ProgrammerError("No fields specified")
458

    
459
    sep = "|"
460
    cmd = [lvm_cmd, "--noheadings", "--nosuffix", "--units=m", "--unbuffered",
461
           "--separator=%s" % sep, "-o%s" % ",".join(fields)]
462

    
463
    result = utils.RunCmd(cmd)
464
    if result.failed:
465
      raise errors.CommandError("Can't get the volume information: %s - %s" %
466
                                (result.fail_reason, result.output))
467

    
468
    data = []
469
    for line in result.stdout.splitlines():
470
      splitted_fields = line.strip().split(sep)
471

    
472
      if len(fields) != len(splitted_fields):
473
        raise errors.CommandError("Can't parse %s output: line '%s'" %
474
                                  (lvm_cmd, line))
475

    
476
      data.append(splitted_fields)
477

    
478
    return data
479

    
480
  @classmethod
481
  def GetPVInfo(cls, vg_names, filter_allocatable=True):
482
    """Get the free space info for PVs in a volume group.
483

484
    @param vg_names: list of volume group names, if empty all will be returned
485
    @param filter_allocatable: whether to skip over unallocatable PVs
486

487
    @rtype: list
488
    @return: list of tuples (free_space, name) with free_space in mebibytes
489

490
    """
491
    try:
492
      info = cls._GetVolumeInfo("pvs", ["pv_name", "vg_name", "pv_free",
493
                                        "pv_attr"])
494
    except errors.GenericError, err:
495
      logging.error("Can't get PV information: %s", err)
496
      return None
497

    
498
    data = []
499
    for pv_name, vg_name, pv_free, pv_attr in info:
500
      # (possibly) skip over pvs which are not allocatable
501
      if filter_allocatable and pv_attr[0] != "a":
502
        continue
503
      # (possibly) skip over pvs which are not in the right volume group(s)
504
      if vg_names and vg_name not in vg_names:
505
        continue
506
      data.append((float(pv_free), pv_name, vg_name))
507

    
508
    return data
509

    
510
  @classmethod
511
  def GetVGInfo(cls, vg_names, filter_readonly=True):
512
    """Get the free space info for specific VGs.
513

514
    @param vg_names: list of volume group names, if empty all will be returned
515
    @param filter_readonly: whether to skip over readonly VGs
516

517
    @rtype: list
518
    @return: list of tuples (free_space, total_size, name) with free_space in
519
             MiB
520

521
    """
522
    try:
523
      info = cls._GetVolumeInfo("vgs", ["vg_name", "vg_free", "vg_attr",
524
                                        "vg_size"])
525
    except errors.GenericError, err:
526
      logging.error("Can't get VG information: %s", err)
527
      return None
528

    
529
    data = []
530
    for vg_name, vg_free, vg_attr, vg_size in info:
531
      # (possibly) skip over vgs which are not writable
532
      if filter_readonly and vg_attr[0] == "r":
533
        continue
534
      # (possibly) skip over vgs which are not in the right volume group(s)
535
      if vg_names and vg_name not in vg_names:
536
        continue
537
      data.append((float(vg_free), float(vg_size), vg_name))
538

    
539
    return data
540

    
541
  @classmethod
542
  def _ValidateName(cls, name):
543
    """Validates that a given name is valid as VG or LV name.
544

545
    The list of valid characters and restricted names is taken out of
546
    the lvm(8) manpage, with the simplification that we enforce both
547
    VG and LV restrictions on the names.
548

549
    """
550
    if (not cls._VALID_NAME_RE.match(name) or
551
        name in cls._INVALID_NAMES or
552
        compat.any(substring in name for substring in cls._INVALID_SUBSTRINGS)):
553
      _ThrowError("Invalid LVM name '%s'", name)
554

    
555
  def Remove(self):
556
    """Remove this logical volume.
557

558
    """
559
    if not self.minor and not self.Attach():
560
      # the LV does not exist
561
      return
562
    result = utils.RunCmd(["lvremove", "-f", "%s/%s" %
563
                           (self._vg_name, self._lv_name)])
564
    if result.failed:
565
      _ThrowError("Can't lvremove: %s - %s", result.fail_reason, result.output)
566

    
567
  def Rename(self, new_id):
568
    """Rename this logical volume.
569

570
    """
571
    if not isinstance(new_id, (tuple, list)) or len(new_id) != 2:
572
      raise errors.ProgrammerError("Invalid new logical id '%s'" % new_id)
573
    new_vg, new_name = new_id
574
    if new_vg != self._vg_name:
575
      raise errors.ProgrammerError("Can't move a logical volume across"
576
                                   " volume groups (from %s to to %s)" %
577
                                   (self._vg_name, new_vg))
578
    result = utils.RunCmd(["lvrename", new_vg, self._lv_name, new_name])
579
    if result.failed:
580
      _ThrowError("Failed to rename the logical volume: %s", result.output)
581
    self._lv_name = new_name
582
    self.dev_path = utils.PathJoin("/dev", self._vg_name, self._lv_name)
583

    
584
  def Attach(self):
585
    """Attach to an existing LV.
586

587
    This method will try to see if an existing and active LV exists
588
    which matches our name. If so, its major/minor will be
589
    recorded.
590

591
    """
592
    self.attached = False
593
    result = utils.RunCmd(["lvs", "--noheadings", "--separator=,",
594
                           "--units=m", "--nosuffix",
595
                           "-olv_attr,lv_kernel_major,lv_kernel_minor,"
596
                           "vg_extent_size,stripes", self.dev_path])
597
    if result.failed:
598
      logging.error("Can't find LV %s: %s, %s",
599
                    self.dev_path, result.fail_reason, result.output)
600
      return False
601
    # the output can (and will) have multiple lines for multi-segment
602
    # LVs, as the 'stripes' parameter is a segment one, so we take
603
    # only the last entry, which is the one we're interested in; note
604
    # that with LVM2 anyway the 'stripes' value must be constant
605
    # across segments, so this is a no-op actually
606
    out = result.stdout.splitlines()
607
    if not out: # totally empty result? splitlines() returns at least
608
                # one line for any non-empty string
609
      logging.error("Can't parse LVS output, no lines? Got '%s'", str(out))
610
      return False
611
    out = out[-1].strip().rstrip(",")
612
    out = out.split(",")
613
    if len(out) != 5:
614
      logging.error("Can't parse LVS output, len(%s) != 5", str(out))
615
      return False
616

    
617
    status, major, minor, pe_size, stripes = out
618
    if len(status) != 6:
619
      logging.error("lvs lv_attr is not 6 characters (%s)", status)
620
      return False
621

    
622
    try:
623
      major = int(major)
624
      minor = int(minor)
625
    except (TypeError, ValueError), err:
626
      logging.error("lvs major/minor cannot be parsed: %s", str(err))
627

    
628
    try:
629
      pe_size = int(float(pe_size))
630
    except (TypeError, ValueError), err:
631
      logging.error("Can't parse vg extent size: %s", err)
632
      return False
633

    
634
    try:
635
      stripes = int(stripes)
636
    except (TypeError, ValueError), err:
637
      logging.error("Can't parse the number of stripes: %s", err)
638
      return False
639

    
640
    self.major = major
641
    self.minor = minor
642
    self.pe_size = pe_size
643
    self.stripe_count = stripes
644
    self._degraded = status[0] == "v" # virtual volume, i.e. doesn't backing
645
                                      # storage
646
    self.attached = True
647
    return True
648

    
649
  def Assemble(self):
650
    """Assemble the device.
651

652
    We always run `lvchange -ay` on the LV to ensure it's active before
653
    use, as there were cases when xenvg was not active after boot
654
    (also possibly after disk issues).
655

656
    """
657
    result = utils.RunCmd(["lvchange", "-ay", self.dev_path])
658
    if result.failed:
659
      _ThrowError("Can't activate lv %s: %s", self.dev_path, result.output)
660

    
661
  def Shutdown(self):
662
    """Shutdown the device.
663

664
    This is a no-op for the LV device type, as we don't deactivate the
665
    volumes on shutdown.
666

667
    """
668
    pass
669

    
670
  def GetSyncStatus(self):
671
    """Returns the sync status of the device.
672

673
    If this device is a mirroring device, this function returns the
674
    status of the mirror.
675

676
    For logical volumes, sync_percent and estimated_time are always
677
    None (no recovery in progress, as we don't handle the mirrored LV
678
    case). The is_degraded parameter is the inverse of the ldisk
679
    parameter.
680

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

687
    The status was already read in Attach, so we just return it.
688

689
    @rtype: objects.BlockDevStatus
690

691
    """
692
    if self._degraded:
693
      ldisk_status = constants.LDS_FAULTY
694
    else:
695
      ldisk_status = constants.LDS_OKAY
696

    
697
    return objects.BlockDevStatus(dev_path=self.dev_path,
698
                                  major=self.major,
699
                                  minor=self.minor,
700
                                  sync_percent=None,
701
                                  estimated_time=None,
702
                                  is_degraded=self._degraded,
703
                                  ldisk_status=ldisk_status)
704

    
705
  def Open(self, force=False):
706
    """Make the device ready for I/O.
707

708
    This is a no-op for the LV device type.
709

710
    """
711
    pass
712

    
713
  def Close(self):
714
    """Notifies that the device will no longer be used for I/O.
715

716
    This is a no-op for the LV device type.
717

718
    """
719
    pass
720

    
721
  def Snapshot(self, size):
722
    """Create a snapshot copy of an lvm block device.
723

724
    @returns: tuple (vg, lv)
725

726
    """
727
    snap_name = self._lv_name + ".snap"
728

    
729
    # remove existing snapshot if found
730
    snap = LogicalVolume((self._vg_name, snap_name), None, size, self.params)
731
    _IgnoreError(snap.Remove)
732

    
733
    vg_info = self.GetVGInfo([self._vg_name])
734
    if not vg_info:
735
      _ThrowError("Can't compute VG info for vg %s", self._vg_name)
736
    free_size, _, _ = vg_info[0]
737
    if free_size < size:
738
      _ThrowError("Not enough free space: required %s,"
739
                  " available %s", size, free_size)
740

    
741
    result = utils.RunCmd(["lvcreate", "-L%dm" % size, "-s",
742
                           "-n%s" % snap_name, self.dev_path])
743
    if result.failed:
744
      _ThrowError("command: %s error: %s - %s",
745
                  result.cmd, result.fail_reason, result.output)
746

    
747
    return (self._vg_name, snap_name)
748

    
749
  def SetInfo(self, text):
750
    """Update metadata with info text.
751

752
    """
753
    BlockDev.SetInfo(self, text)
754

    
755
    # Replace invalid characters
756
    text = re.sub("^[^A-Za-z0-9_+.]", "_", text)
757
    text = re.sub("[^-A-Za-z0-9_+.]", "_", text)
758

    
759
    # Only up to 128 characters are allowed
760
    text = text[:128]
761

    
762
    result = utils.RunCmd(["lvchange", "--addtag", text,
763
                           self.dev_path])
764
    if result.failed:
765
      _ThrowError("Command: %s error: %s - %s", result.cmd, result.fail_reason,
766
                  result.output)
767

    
768
  def Grow(self, amount, dryrun):
769
    """Grow the logical volume.
770

771
    """
772
    if self.pe_size is None or self.stripe_count is None:
773
      if not self.Attach():
774
        _ThrowError("Can't attach to LV during Grow()")
775
    full_stripe_size = self.pe_size * self.stripe_count
776
    rest = amount % full_stripe_size
777
    if rest != 0:
778
      amount += full_stripe_size - rest
779
    cmd = ["lvextend", "-L", "+%dm" % amount]
780
    if dryrun:
781
      cmd.append("--test")
782
    # we try multiple algorithms since the 'best' ones might not have
783
    # space available in the right place, but later ones might (since
784
    # they have less constraints); also note that only recent LVM
785
    # supports 'cling'
786
    for alloc_policy in "contiguous", "cling", "normal":
787
      result = utils.RunCmd(cmd + ["--alloc", alloc_policy, self.dev_path])
788
      if not result.failed:
789
        return
790
    _ThrowError("Can't grow LV %s: %s", self.dev_path, result.output)
791

    
792

    
793
class DRBD8Status(object):
794
  """A DRBD status representation class.
795

796
  Note that this doesn't support unconfigured devices (cs:Unconfigured).
797

798
  """
799
  UNCONF_RE = re.compile(r"\s*[0-9]+:\s*cs:Unconfigured$")
800
  LINE_RE = re.compile(r"\s*[0-9]+:\s*cs:(\S+)\s+(?:st|ro):([^/]+)/(\S+)"
801
                       "\s+ds:([^/]+)/(\S+)\s+.*$")
802
  SYNC_RE = re.compile(r"^.*\ssync'ed:\s*([0-9.]+)%.*"
803
                       # Due to a bug in drbd in the kernel, introduced in
804
                       # commit 4b0715f096 (still unfixed as of 2011-08-22)
805
                       "(?:\s|M)"
806
                       "finish: ([0-9]+):([0-9]+):([0-9]+)\s.*$")
807

    
808
  CS_UNCONFIGURED = "Unconfigured"
809
  CS_STANDALONE = "StandAlone"
810
  CS_WFCONNECTION = "WFConnection"
811
  CS_WFREPORTPARAMS = "WFReportParams"
812
  CS_CONNECTED = "Connected"
813
  CS_STARTINGSYNCS = "StartingSyncS"
814
  CS_STARTINGSYNCT = "StartingSyncT"
815
  CS_WFBITMAPS = "WFBitMapS"
816
  CS_WFBITMAPT = "WFBitMapT"
817
  CS_WFSYNCUUID = "WFSyncUUID"
818
  CS_SYNCSOURCE = "SyncSource"
819
  CS_SYNCTARGET = "SyncTarget"
820
  CS_PAUSEDSYNCS = "PausedSyncS"
821
  CS_PAUSEDSYNCT = "PausedSyncT"
822
  CSET_SYNC = frozenset([
823
    CS_WFREPORTPARAMS,
824
    CS_STARTINGSYNCS,
825
    CS_STARTINGSYNCT,
826
    CS_WFBITMAPS,
827
    CS_WFBITMAPT,
828
    CS_WFSYNCUUID,
829
    CS_SYNCSOURCE,
830
    CS_SYNCTARGET,
831
    CS_PAUSEDSYNCS,
832
    CS_PAUSEDSYNCT,
833
    ])
834

    
835
  DS_DISKLESS = "Diskless"
836
  DS_ATTACHING = "Attaching" # transient state
837
  DS_FAILED = "Failed" # transient state, next: diskless
838
  DS_NEGOTIATING = "Negotiating" # transient state
839
  DS_INCONSISTENT = "Inconsistent" # while syncing or after creation
840
  DS_OUTDATED = "Outdated"
841
  DS_DUNKNOWN = "DUnknown" # shown for peer disk when not connected
842
  DS_CONSISTENT = "Consistent"
843
  DS_UPTODATE = "UpToDate" # normal state
844

    
845
  RO_PRIMARY = "Primary"
846
  RO_SECONDARY = "Secondary"
847
  RO_UNKNOWN = "Unknown"
848

    
849
  def __init__(self, procline):
850
    u = self.UNCONF_RE.match(procline)
851
    if u:
852
      self.cstatus = self.CS_UNCONFIGURED
853
      self.lrole = self.rrole = self.ldisk = self.rdisk = None
854
    else:
855
      m = self.LINE_RE.match(procline)
856
      if not m:
857
        raise errors.BlockDeviceError("Can't parse input data '%s'" % procline)
858
      self.cstatus = m.group(1)
859
      self.lrole = m.group(2)
860
      self.rrole = m.group(3)
861
      self.ldisk = m.group(4)
862
      self.rdisk = m.group(5)
863

    
864
    # end reading of data from the LINE_RE or UNCONF_RE
865

    
866
    self.is_standalone = self.cstatus == self.CS_STANDALONE
867
    self.is_wfconn = self.cstatus == self.CS_WFCONNECTION
868
    self.is_connected = self.cstatus == self.CS_CONNECTED
869
    self.is_primary = self.lrole == self.RO_PRIMARY
870
    self.is_secondary = self.lrole == self.RO_SECONDARY
871
    self.peer_primary = self.rrole == self.RO_PRIMARY
872
    self.peer_secondary = self.rrole == self.RO_SECONDARY
873
    self.both_primary = self.is_primary and self.peer_primary
874
    self.both_secondary = self.is_secondary and self.peer_secondary
875

    
876
    self.is_diskless = self.ldisk == self.DS_DISKLESS
877
    self.is_disk_uptodate = self.ldisk == self.DS_UPTODATE
878

    
879
    self.is_in_resync = self.cstatus in self.CSET_SYNC
880
    self.is_in_use = self.cstatus != self.CS_UNCONFIGURED
881

    
882
    m = self.SYNC_RE.match(procline)
883
    if m:
884
      self.sync_percent = float(m.group(1))
885
      hours = int(m.group(2))
886
      minutes = int(m.group(3))
887
      seconds = int(m.group(4))
888
      self.est_time = hours * 3600 + minutes * 60 + seconds
889
    else:
890
      # we have (in this if branch) no percent information, but if
891
      # we're resyncing we need to 'fake' a sync percent information,
892
      # as this is how cmdlib determines if it makes sense to wait for
893
      # resyncing or not
894
      if self.is_in_resync:
895
        self.sync_percent = 0
896
      else:
897
        self.sync_percent = None
898
      self.est_time = None
899

    
900

    
901
class BaseDRBD(BlockDev): # pylint: disable=W0223
902
  """Base DRBD class.
903

904
  This class contains a few bits of common functionality between the
905
  0.7 and 8.x versions of DRBD.
906

907
  """
908
  _VERSION_RE = re.compile(r"^version: (\d+)\.(\d+)\.(\d+)(?:\.\d+)?"
909
                           r" \(api:(\d+)/proto:(\d+)(?:-(\d+))?\)")
910
  _VALID_LINE_RE = re.compile("^ *([0-9]+): cs:([^ ]+).*$")
911
  _UNUSED_LINE_RE = re.compile("^ *([0-9]+): cs:Unconfigured$")
912

    
913
  _DRBD_MAJOR = 147
914
  _ST_UNCONFIGURED = "Unconfigured"
915
  _ST_WFCONNECTION = "WFConnection"
916
  _ST_CONNECTED = "Connected"
917

    
918
  _STATUS_FILE = "/proc/drbd"
919
  _USERMODE_HELPER_FILE = "/sys/module/drbd/parameters/usermode_helper"
920

    
921
  @staticmethod
922
  def _GetProcData(filename=_STATUS_FILE):
923
    """Return data from /proc/drbd.
924

925
    """
926
    try:
927
      data = utils.ReadFile(filename).splitlines()
928
    except EnvironmentError, err:
929
      if err.errno == errno.ENOENT:
930
        _ThrowError("The file %s cannot be opened, check if the module"
931
                    " is loaded (%s)", filename, str(err))
932
      else:
933
        _ThrowError("Can't read the DRBD proc file %s: %s", filename, str(err))
934
    if not data:
935
      _ThrowError("Can't read any data from %s", filename)
936
    return data
937

    
938
  @classmethod
939
  def _MassageProcData(cls, data):
940
    """Transform the output of _GetProdData into a nicer form.
941

942
    @return: a dictionary of minor: joined lines from /proc/drbd
943
        for that minor
944

945
    """
946
    results = {}
947
    old_minor = old_line = None
948
    for line in data:
949
      if not line: # completely empty lines, as can be returned by drbd8.0+
950
        continue
951
      lresult = cls._VALID_LINE_RE.match(line)
952
      if lresult is not None:
953
        if old_minor is not None:
954
          results[old_minor] = old_line
955
        old_minor = int(lresult.group(1))
956
        old_line = line
957
      else:
958
        if old_minor is not None:
959
          old_line += " " + line.strip()
960
    # add last line
961
    if old_minor is not None:
962
      results[old_minor] = old_line
963
    return results
964

    
965
  @classmethod
966
  def _GetVersion(cls, proc_data):
967
    """Return the DRBD version.
968

969
    This will return a dict with keys:
970
      - k_major
971
      - k_minor
972
      - k_point
973
      - api
974
      - proto
975
      - proto2 (only on drbd > 8.2.X)
976

977
    """
978
    first_line = proc_data[0].strip()
979
    version = cls._VERSION_RE.match(first_line)
980
    if not version:
981
      raise errors.BlockDeviceError("Can't parse DRBD version from '%s'" %
982
                                    first_line)
983

    
984
    values = version.groups()
985
    retval = {"k_major": int(values[0]),
986
              "k_minor": int(values[1]),
987
              "k_point": int(values[2]),
988
              "api": int(values[3]),
989
              "proto": int(values[4]),
990
             }
991
    if values[5] is not None:
992
      retval["proto2"] = values[5]
993

    
994
    return retval
995

    
996
  @staticmethod
997
  def GetUsermodeHelper(filename=_USERMODE_HELPER_FILE):
998
    """Returns DRBD usermode_helper currently set.
999

1000
    """
1001
    try:
1002
      helper = utils.ReadFile(filename).splitlines()[0]
1003
    except EnvironmentError, err:
1004
      if err.errno == errno.ENOENT:
1005
        _ThrowError("The file %s cannot be opened, check if the module"
1006
                    " is loaded (%s)", filename, str(err))
1007
      else:
1008
        _ThrowError("Can't read DRBD helper file %s: %s", filename, str(err))
1009
    if not helper:
1010
      _ThrowError("Can't read any data from %s", filename)
1011
    return helper
1012

    
1013
  @staticmethod
1014
  def _DevPath(minor):
1015
    """Return the path to a drbd device for a given minor.
1016

1017
    """
1018
    return "/dev/drbd%d" % minor
1019

    
1020
  @classmethod
1021
  def GetUsedDevs(cls):
1022
    """Compute the list of used DRBD devices.
1023

1024
    """
1025
    data = cls._GetProcData()
1026

    
1027
    used_devs = {}
1028
    for line in data:
1029
      match = cls._VALID_LINE_RE.match(line)
1030
      if not match:
1031
        continue
1032
      minor = int(match.group(1))
1033
      state = match.group(2)
1034
      if state == cls._ST_UNCONFIGURED:
1035
        continue
1036
      used_devs[minor] = state, line
1037

    
1038
    return used_devs
1039

    
1040
  def _SetFromMinor(self, minor):
1041
    """Set our parameters based on the given minor.
1042

1043
    This sets our minor variable and our dev_path.
1044

1045
    """
1046
    if minor is None:
1047
      self.minor = self.dev_path = None
1048
      self.attached = False
1049
    else:
1050
      self.minor = minor
1051
      self.dev_path = self._DevPath(minor)
1052
      self.attached = True
1053

    
1054
  @staticmethod
1055
  def _CheckMetaSize(meta_device):
1056
    """Check if the given meta device looks like a valid one.
1057

1058
    This currently only check the size, which must be around
1059
    128MiB.
1060

1061
    """
1062
    result = utils.RunCmd(["blockdev", "--getsize", meta_device])
1063
    if result.failed:
1064
      _ThrowError("Failed to get device size: %s - %s",
1065
                  result.fail_reason, result.output)
1066
    try:
1067
      sectors = int(result.stdout)
1068
    except (TypeError, ValueError):
1069
      _ThrowError("Invalid output from blockdev: '%s'", result.stdout)
1070
    num_bytes = sectors * 512
1071
    if num_bytes < 128 * 1024 * 1024: # less than 128MiB
1072
      _ThrowError("Meta device too small (%.2fMib)", (num_bytes / 1024 / 1024))
1073
    # the maximum *valid* size of the meta device when living on top
1074
    # of LVM is hard to compute: it depends on the number of stripes
1075
    # and the PE size; e.g. a 2-stripe, 64MB PE will result in a 128MB
1076
    # (normal size), but an eight-stripe 128MB PE will result in a 1GB
1077
    # size meta device; as such, we restrict it to 1GB (a little bit
1078
    # too generous, but making assumptions about PE size is hard)
1079
    if num_bytes > 1024 * 1024 * 1024:
1080
      _ThrowError("Meta device too big (%.2fMiB)", (num_bytes / 1024 / 1024))
1081

    
1082
  def Rename(self, new_id):
1083
    """Rename a device.
1084

1085
    This is not supported for drbd devices.
1086

1087
    """
1088
    raise errors.ProgrammerError("Can't rename a drbd device")
1089

    
1090

    
1091
class DRBD8(BaseDRBD):
1092
  """DRBD v8.x block device.
1093

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

1098
  The unique_id for the drbd device is the (local_ip, local_port,
1099
  remote_ip, remote_port) tuple, and it must have two children: the
1100
  data device and the meta_device. The meta device is checked for
1101
  valid size and is zeroed on create.
1102

1103
  """
1104
  _MAX_MINORS = 255
1105
  _PARSE_SHOW = None
1106

    
1107
  # timeout constants
1108
  _NET_RECONFIG_TIMEOUT = 60
1109

    
1110
  # command line options for barriers
1111
  _DISABLE_DISK_OPTION = "--no-disk-barrier"  # -a
1112
  _DISABLE_DRAIN_OPTION = "--no-disk-drain"   # -D
1113
  _DISABLE_FLUSH_OPTION = "--no-disk-flushes" # -i
1114
  _DISABLE_META_FLUSH_OPTION = "--no-md-flushes"  # -m
1115

    
1116
  def __init__(self, unique_id, children, size, params):
1117
    if children and children.count(None) > 0:
1118
      children = []
1119
    if len(children) not in (0, 2):
1120
      raise ValueError("Invalid configuration data %s" % str(children))
1121
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 6:
1122
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1123
    (self._lhost, self._lport,
1124
     self._rhost, self._rport,
1125
     self._aminor, self._secret) = unique_id
1126
    if children:
1127
      if not _CanReadDevice(children[1].dev_path):
1128
        logging.info("drbd%s: Ignoring unreadable meta device", self._aminor)
1129
        children = []
1130
    super(DRBD8, self).__init__(unique_id, children, size, params)
1131
    self.major = self._DRBD_MAJOR
1132
    version = self._GetVersion(self._GetProcData())
1133
    if version["k_major"] != 8:
1134
      _ThrowError("Mismatch in DRBD kernel version and requested ganeti"
1135
                  " usage: kernel is %s.%s, ganeti wants 8.x",
1136
                  version["k_major"], version["k_minor"])
1137

    
1138
    if (self._lhost is not None and self._lhost == self._rhost and
1139
        self._lport == self._rport):
1140
      raise ValueError("Invalid configuration data, same local/remote %s" %
1141
                       (unique_id,))
1142
    self.Attach()
1143

    
1144
  @classmethod
1145
  def _InitMeta(cls, minor, dev_path):
1146
    """Initialize a meta device.
1147

1148
    This will not work if the given minor is in use.
1149

1150
    """
1151
    # Zero the metadata first, in order to make sure drbdmeta doesn't
1152
    # try to auto-detect existing filesystems or similar (see
1153
    # http://code.google.com/p/ganeti/issues/detail?id=182); we only
1154
    # care about the first 128MB of data in the device, even though it
1155
    # can be bigger
1156
    result = utils.RunCmd([constants.DD_CMD,
1157
                           "if=/dev/zero", "of=%s" % dev_path,
1158
                           "bs=1048576", "count=128", "oflag=direct"])
1159
    if result.failed:
1160
      _ThrowError("Can't wipe the meta device: %s", result.output)
1161

    
1162
    result = utils.RunCmd(["drbdmeta", "--force", cls._DevPath(minor),
1163
                           "v08", dev_path, "0", "create-md"])
1164
    if result.failed:
1165
      _ThrowError("Can't initialize meta device: %s", result.output)
1166

    
1167
  @classmethod
1168
  def _FindUnusedMinor(cls):
1169
    """Find an unused DRBD device.
1170

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

1174
    """
1175
    data = cls._GetProcData()
1176

    
1177
    highest = None
1178
    for line in data:
1179
      match = cls._UNUSED_LINE_RE.match(line)
1180
      if match:
1181
        return int(match.group(1))
1182
      match = cls._VALID_LINE_RE.match(line)
1183
      if match:
1184
        minor = int(match.group(1))
1185
        highest = max(highest, minor)
1186
    if highest is None: # there are no minors in use at all
1187
      return 0
1188
    if highest >= cls._MAX_MINORS:
1189
      logging.error("Error: no free drbd minors!")
1190
      raise errors.BlockDeviceError("Can't find a free DRBD minor")
1191
    return highest + 1
1192

    
1193
  @classmethod
1194
  def _GetShowParser(cls):
1195
    """Return a parser for `drbd show` output.
1196

1197
    This will either create or return an already-create parser for the
1198
    output of the command `drbd show`.
1199

1200
    """
1201
    if cls._PARSE_SHOW is not None:
1202
      return cls._PARSE_SHOW
1203

    
1204
    # pyparsing setup
1205
    lbrace = pyp.Literal("{").suppress()
1206
    rbrace = pyp.Literal("}").suppress()
1207
    lbracket = pyp.Literal("[").suppress()
1208
    rbracket = pyp.Literal("]").suppress()
1209
    semi = pyp.Literal(";").suppress()
1210
    colon = pyp.Literal(":").suppress()
1211
    # this also converts the value to an int
1212
    number = pyp.Word(pyp.nums).setParseAction(lambda s, l, t: int(t[0]))
1213

    
1214
    comment = pyp.Literal("#") + pyp.Optional(pyp.restOfLine)
1215
    defa = pyp.Literal("_is_default").suppress()
1216
    dbl_quote = pyp.Literal('"').suppress()
1217

    
1218
    keyword = pyp.Word(pyp.alphanums + '-')
1219

    
1220
    # value types
1221
    value = pyp.Word(pyp.alphanums + '_-/.:')
1222
    quoted = dbl_quote + pyp.CharsNotIn('"') + dbl_quote
1223
    ipv4_addr = (pyp.Optional(pyp.Literal("ipv4")).suppress() +
1224
                 pyp.Word(pyp.nums + ".") + colon + number)
1225
    ipv6_addr = (pyp.Optional(pyp.Literal("ipv6")).suppress() +
1226
                 pyp.Optional(lbracket) + pyp.Word(pyp.hexnums + ":") +
1227
                 pyp.Optional(rbracket) + colon + number)
1228
    # meta device, extended syntax
1229
    meta_value = ((value ^ quoted) + lbracket + number + rbracket)
1230
    # device name, extended syntax
1231
    device_value = pyp.Literal("minor").suppress() + number
1232

    
1233
    # a statement
1234
    stmt = (~rbrace + keyword + ~lbrace +
1235
            pyp.Optional(ipv4_addr ^ ipv6_addr ^ value ^ quoted ^ meta_value ^
1236
                         device_value) +
1237
            pyp.Optional(defa) + semi +
1238
            pyp.Optional(pyp.restOfLine).suppress())
1239

    
1240
    # an entire section
1241
    section_name = pyp.Word(pyp.alphas + "_")
1242
    section = section_name + lbrace + pyp.ZeroOrMore(pyp.Group(stmt)) + rbrace
1243

    
1244
    bnf = pyp.ZeroOrMore(pyp.Group(section ^ stmt))
1245
    bnf.ignore(comment)
1246

    
1247
    cls._PARSE_SHOW = bnf
1248

    
1249
    return bnf
1250

    
1251
  @classmethod
1252
  def _GetShowData(cls, minor):
1253
    """Return the `drbdsetup show` data for a minor.
1254

1255
    """
1256
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "show"])
1257
    if result.failed:
1258
      logging.error("Can't display the drbd config: %s - %s",
1259
                    result.fail_reason, result.output)
1260
      return None
1261
    return result.stdout
1262

    
1263
  @classmethod
1264
  def _GetDevInfo(cls, out):
1265
    """Parse details about a given DRBD minor.
1266

1267
    This return, if available, the local backing device (as a path)
1268
    and the local and remote (ip, port) information from a string
1269
    containing the output of the `drbdsetup show` command as returned
1270
    by _GetShowData.
1271

1272
    """
1273
    data = {}
1274
    if not out:
1275
      return data
1276

    
1277
    bnf = cls._GetShowParser()
1278
    # run pyparse
1279

    
1280
    try:
1281
      results = bnf.parseString(out)
1282
    except pyp.ParseException, err:
1283
      _ThrowError("Can't parse drbdsetup show output: %s", str(err))
1284

    
1285
    # and massage the results into our desired format
1286
    for section in results:
1287
      sname = section[0]
1288
      if sname == "_this_host":
1289
        for lst in section[1:]:
1290
          if lst[0] == "disk":
1291
            data["local_dev"] = lst[1]
1292
          elif lst[0] == "meta-disk":
1293
            data["meta_dev"] = lst[1]
1294
            data["meta_index"] = lst[2]
1295
          elif lst[0] == "address":
1296
            data["local_addr"] = tuple(lst[1:])
1297
      elif sname == "_remote_host":
1298
        for lst in section[1:]:
1299
          if lst[0] == "address":
1300
            data["remote_addr"] = tuple(lst[1:])
1301
    return data
1302

    
1303
  def _MatchesLocal(self, info):
1304
    """Test if our local config matches with an existing device.
1305

1306
    The parameter should be as returned from `_GetDevInfo()`. This
1307
    method tests if our local backing device is the same as the one in
1308
    the info parameter, in effect testing if we look like the given
1309
    device.
1310

1311
    """
1312
    if self._children:
1313
      backend, meta = self._children
1314
    else:
1315
      backend = meta = None
1316

    
1317
    if backend is not None:
1318
      retval = ("local_dev" in info and info["local_dev"] == backend.dev_path)
1319
    else:
1320
      retval = ("local_dev" not in info)
1321

    
1322
    if meta is not None:
1323
      retval = retval and ("meta_dev" in info and
1324
                           info["meta_dev"] == meta.dev_path)
1325
      retval = retval and ("meta_index" in info and
1326
                           info["meta_index"] == 0)
1327
    else:
1328
      retval = retval and ("meta_dev" not in info and
1329
                           "meta_index" not in info)
1330
    return retval
1331

    
1332
  def _MatchesNet(self, info):
1333
    """Test if our network config matches with an existing device.
1334

1335
    The parameter should be as returned from `_GetDevInfo()`. This
1336
    method tests if our network configuration is the same as the one
1337
    in the info parameter, in effect testing if we look like the given
1338
    device.
1339

1340
    """
1341
    if (((self._lhost is None and not ("local_addr" in info)) and
1342
         (self._rhost is None and not ("remote_addr" in info)))):
1343
      return True
1344

    
1345
    if self._lhost is None:
1346
      return False
1347

    
1348
    if not ("local_addr" in info and
1349
            "remote_addr" in info):
1350
      return False
1351

    
1352
    retval = (info["local_addr"] == (self._lhost, self._lport))
1353
    retval = (retval and
1354
              info["remote_addr"] == (self._rhost, self._rport))
1355
    return retval
1356

    
1357
  def _AssembleLocal(self, minor, backend, meta, size):
1358
    """Configure the local part of a DRBD device.
1359

1360
    """
1361
    args = ["drbdsetup", self._DevPath(minor), "disk",
1362
            backend, meta, "0",
1363
            "-e", "detach",
1364
            "--create-device"]
1365
    if size:
1366
      args.extend(["-d", "%sm" % size])
1367

    
1368
    version = self._GetVersion(self._GetProcData())
1369
    vmaj = version["k_major"]
1370
    vmin = version["k_minor"]
1371
    vrel = version["k_point"]
1372

    
1373
    barrier_args = \
1374
      self._ComputeDiskBarrierArgs(vmaj, vmin, vrel,
1375
                                   self.params[constants.LDP_BARRIERS],
1376
                                   self.params[constants.LDP_NO_META_FLUSH])
1377
    args.extend(barrier_args)
1378

    
1379
    if self.params[constants.LDP_DISK_CUSTOM]:
1380
      args.extend(shlex.split(self.params[constants.LDP_DISK_CUSTOM]))
1381

    
1382
    result = utils.RunCmd(args)
1383
    if result.failed:
1384
      _ThrowError("drbd%d: can't attach local disk: %s", minor, result.output)
1385

    
1386
  @classmethod
1387
  def _ComputeDiskBarrierArgs(cls, vmaj, vmin, vrel, disabled_barriers,
1388
      disable_meta_flush):
1389
    """Compute the DRBD command line parameters for disk barriers
1390

1391
    Returns a list of the disk barrier parameters as requested via the
1392
    disabled_barriers and disable_meta_flush arguments, and according to the
1393
    supported ones in the DRBD version vmaj.vmin.vrel
1394

1395
    If the desired option is unsupported, raises errors.BlockDeviceError.
1396

1397
    """
1398
    disabled_barriers_set = frozenset(disabled_barriers)
1399
    if not disabled_barriers_set in constants.DRBD_VALID_BARRIER_OPT:
1400
      raise errors.BlockDeviceError("%s is not a valid option set for DRBD"
1401
                                    " barriers" % disabled_barriers)
1402

    
1403
    args = []
1404

    
1405
    # The following code assumes DRBD 8.x, with x < 4 and x != 1 (DRBD 8.1.x
1406
    # does not exist)
1407
    if not vmaj == 8 and vmin in (0, 2, 3):
1408
      raise errors.BlockDeviceError("Unsupported DRBD version: %d.%d.%d" %
1409
                                    (vmaj, vmin, vrel))
1410

    
1411
    def _AppendOrRaise(option, min_version):
1412
      """Helper for DRBD options"""
1413
      if min_version is not None and vrel >= min_version:
1414
        args.append(option)
1415
      else:
1416
        raise errors.BlockDeviceError("Could not use the option %s as the"
1417
                                      " DRBD version %d.%d.%d does not support"
1418
                                      " it." % (option, vmaj, vmin, vrel))
1419

    
1420
    # the minimum version for each feature is encoded via pairs of (minor
1421
    # version -> x) where x is version in which support for the option was
1422
    # introduced.
1423
    meta_flush_supported = disk_flush_supported = {
1424
      0: 12,
1425
      2: 7,
1426
      3: 0,
1427
      }
1428

    
1429
    disk_drain_supported = {
1430
      2: 7,
1431
      3: 0,
1432
      }
1433

    
1434
    disk_barriers_supported = {
1435
      3: 0,
1436
      }
1437

    
1438
    # meta flushes
1439
    if disable_meta_flush:
1440
      _AppendOrRaise(cls._DISABLE_META_FLUSH_OPTION,
1441
                     meta_flush_supported.get(vmin, None))
1442

    
1443
    # disk flushes
1444
    if constants.DRBD_B_DISK_FLUSH in disabled_barriers_set:
1445
      _AppendOrRaise(cls._DISABLE_FLUSH_OPTION,
1446
                     disk_flush_supported.get(vmin, None))
1447

    
1448
    # disk drain
1449
    if constants.DRBD_B_DISK_DRAIN in disabled_barriers_set:
1450
      _AppendOrRaise(cls._DISABLE_DRAIN_OPTION,
1451
                     disk_drain_supported.get(vmin, None))
1452

    
1453
    # disk barriers
1454
    if constants.DRBD_B_DISK_BARRIERS in disabled_barriers_set:
1455
      _AppendOrRaise(cls._DISABLE_DISK_OPTION,
1456
                     disk_barriers_supported.get(vmin, None))
1457

    
1458
    return args
1459

    
1460
  def _AssembleNet(self, minor, net_info, protocol,
1461
                   dual_pri=False, hmac=None, secret=None):
1462
    """Configure the network part of the device.
1463

1464
    """
1465
    lhost, lport, rhost, rport = net_info
1466
    if None in net_info:
1467
      # we don't want network connection and actually want to make
1468
      # sure its shutdown
1469
      self._ShutdownNet(minor)
1470
      return
1471

    
1472
    # Workaround for a race condition. When DRBD is doing its dance to
1473
    # establish a connection with its peer, it also sends the
1474
    # synchronization speed over the wire. In some cases setting the
1475
    # sync speed only after setting up both sides can race with DRBD
1476
    # connecting, hence we set it here before telling DRBD anything
1477
    # about its peer.
1478
    self._SetMinorSyncParams(minor, self.params)
1479

    
1480
    if netutils.IP6Address.IsValid(lhost):
1481
      if not netutils.IP6Address.IsValid(rhost):
1482
        _ThrowError("drbd%d: can't connect ip %s to ip %s" %
1483
                    (minor, lhost, rhost))
1484
      family = "ipv6"
1485
    elif netutils.IP4Address.IsValid(lhost):
1486
      if not netutils.IP4Address.IsValid(rhost):
1487
        _ThrowError("drbd%d: can't connect ip %s to ip %s" %
1488
                    (minor, lhost, rhost))
1489
      family = "ipv4"
1490
    else:
1491
      _ThrowError("drbd%d: Invalid ip %s" % (minor, lhost))
1492

    
1493
    args = ["drbdsetup", self._DevPath(minor), "net",
1494
            "%s:%s:%s" % (family, lhost, lport),
1495
            "%s:%s:%s" % (family, rhost, rport), protocol,
1496
            "-A", "discard-zero-changes",
1497
            "-B", "consensus",
1498
            "--create-device",
1499
            ]
1500
    if dual_pri:
1501
      args.append("-m")
1502
    if hmac and secret:
1503
      args.extend(["-a", hmac, "-x", secret])
1504

    
1505
    if self.params[constants.LDP_NET_CUSTOM]:
1506
      args.extend(shlex.split(self.params[constants.LDP_NET_CUSTOM]))
1507

    
1508
    result = utils.RunCmd(args)
1509
    if result.failed:
1510
      _ThrowError("drbd%d: can't setup network: %s - %s",
1511
                  minor, result.fail_reason, result.output)
1512

    
1513
    def _CheckNetworkConfig():
1514
      info = self._GetDevInfo(self._GetShowData(minor))
1515
      if not "local_addr" in info or not "remote_addr" in info:
1516
        raise utils.RetryAgain()
1517

    
1518
      if (info["local_addr"] != (lhost, lport) or
1519
          info["remote_addr"] != (rhost, rport)):
1520
        raise utils.RetryAgain()
1521

    
1522
    try:
1523
      utils.Retry(_CheckNetworkConfig, 1.0, 10.0)
1524
    except utils.RetryTimeout:
1525
      _ThrowError("drbd%d: timeout while configuring network", minor)
1526

    
1527
  def AddChildren(self, devices):
1528
    """Add a disk to the DRBD device.
1529

1530
    """
1531
    if self.minor is None:
1532
      _ThrowError("drbd%d: can't attach to dbrd8 during AddChildren",
1533
                  self._aminor)
1534
    if len(devices) != 2:
1535
      _ThrowError("drbd%d: need two devices for AddChildren", self.minor)
1536
    info = self._GetDevInfo(self._GetShowData(self.minor))
1537
    if "local_dev" in info:
1538
      _ThrowError("drbd%d: already attached to a local disk", self.minor)
1539
    backend, meta = devices
1540
    if backend.dev_path is None or meta.dev_path is None:
1541
      _ThrowError("drbd%d: children not ready during AddChildren", self.minor)
1542
    backend.Open()
1543
    meta.Open()
1544
    self._CheckMetaSize(meta.dev_path)
1545
    self._InitMeta(self._FindUnusedMinor(), meta.dev_path)
1546

    
1547
    self._AssembleLocal(self.minor, backend.dev_path, meta.dev_path, self.size)
1548
    self._children = devices
1549

    
1550
  def RemoveChildren(self, devices):
1551
    """Detach the drbd device from local storage.
1552

1553
    """
1554
    if self.minor is None:
1555
      _ThrowError("drbd%d: can't attach to drbd8 during RemoveChildren",
1556
                  self._aminor)
1557
    # early return if we don't actually have backing storage
1558
    info = self._GetDevInfo(self._GetShowData(self.minor))
1559
    if "local_dev" not in info:
1560
      return
1561
    if len(self._children) != 2:
1562
      _ThrowError("drbd%d: we don't have two children: %s", self.minor,
1563
                  self._children)
1564
    if self._children.count(None) == 2: # we don't actually have children :)
1565
      logging.warning("drbd%d: requested detach while detached", self.minor)
1566
      return
1567
    if len(devices) != 2:
1568
      _ThrowError("drbd%d: we need two children in RemoveChildren", self.minor)
1569
    for child, dev in zip(self._children, devices):
1570
      if dev != child.dev_path:
1571
        _ThrowError("drbd%d: mismatch in local storage (%s != %s) in"
1572
                    " RemoveChildren", self.minor, dev, child.dev_path)
1573

    
1574
    self._ShutdownLocal(self.minor)
1575
    self._children = []
1576

    
1577
  @classmethod
1578
  def _SetMinorSyncParams(cls, minor, params):
1579
    """Set the parameters of the DRBD syncer.
1580

1581
    This is the low-level implementation.
1582

1583
    @type minor: int
1584
    @param minor: the drbd minor whose settings we change
1585
    @type params: dict
1586
    @param params: LD level disk parameters related to the synchronization
1587
    @rtype: boolean
1588
    @return: the success of the operation
1589

1590
    """
1591

    
1592
    args = ["drbdsetup", cls._DevPath(minor), "syncer"]
1593
    if params[constants.LDP_DYNAMIC_RESYNC]:
1594
      version = cls._GetVersion(cls._GetProcData())
1595
      vmin = version["k_minor"]
1596
      vrel = version["k_point"]
1597

    
1598
      # By definition we are using 8.x, so just check the rest of the version
1599
      # number
1600
      if vmin != 3 or vrel < 9:
1601
        logging.error("The current DRBD version (8.%d.%d) does not support the"
1602
                      " dynamic resync speed controller", vmin, vrel)
1603
        return False
1604

    
1605
      # add the c-* parameters to args
1606
      # TODO(spadaccio) use the actual parameters
1607
      args.extend(["--c-plan-ahead", "20"])
1608

    
1609
    else:
1610
      args.extend(["-r", "%d" % params[constants.LDP_RESYNC_RATE]])
1611

    
1612
    args.append("--create-device")
1613
    result = utils.RunCmd(args)
1614
    if result.failed:
1615
      logging.error("Can't change syncer rate: %s - %s",
1616
                    result.fail_reason, result.output)
1617
    return not result.failed
1618

    
1619
  def SetSyncParams(self, params):
1620
    """Set the synchronization parameters of the DRBD syncer.
1621

1622
    @type params: dict
1623
    @param params: LD level disk parameters related to the synchronization
1624
    @rtype: boolean
1625
    @return: the success of the operation
1626

1627
    """
1628
    if self.minor is None:
1629
      logging.info("Not attached during SetSyncParams")
1630
      return False
1631
    children_result = super(DRBD8, self).SetSyncParams(params)
1632
    return self._SetMinorSyncParams(self.minor, params) and children_result
1633

    
1634
  def PauseResumeSync(self, pause):
1635
    """Pauses or resumes the sync of a DRBD device.
1636

1637
    @param pause: Wether to pause or resume
1638
    @return: the success of the operation
1639

1640
    """
1641
    if self.minor is None:
1642
      logging.info("Not attached during PauseSync")
1643
      return False
1644

    
1645
    children_result = super(DRBD8, self).PauseResumeSync(pause)
1646

    
1647
    if pause:
1648
      cmd = "pause-sync"
1649
    else:
1650
      cmd = "resume-sync"
1651

    
1652
    result = utils.RunCmd(["drbdsetup", self.dev_path, cmd])
1653
    if result.failed:
1654
      logging.error("Can't %s: %s - %s", cmd,
1655
                    result.fail_reason, result.output)
1656
    return not result.failed and children_result
1657

    
1658
  def GetProcStatus(self):
1659
    """Return device data from /proc.
1660

1661
    """
1662
    if self.minor is None:
1663
      _ThrowError("drbd%d: GetStats() called while not attached", self._aminor)
1664
    proc_info = self._MassageProcData(self._GetProcData())
1665
    if self.minor not in proc_info:
1666
      _ThrowError("drbd%d: can't find myself in /proc", self.minor)
1667
    return DRBD8Status(proc_info[self.minor])
1668

    
1669
  def GetSyncStatus(self):
1670
    """Returns the sync status of the device.
1671

1672

1673
    If sync_percent is None, it means all is ok
1674
    If estimated_time is None, it means we can't estimate
1675
    the time needed, otherwise it's the time left in seconds.
1676

1677

1678
    We set the is_degraded parameter to True on two conditions:
1679
    network not connected or local disk missing.
1680

1681
    We compute the ldisk parameter based on whether we have a local
1682
    disk or not.
1683

1684
    @rtype: objects.BlockDevStatus
1685

1686
    """
1687
    if self.minor is None and not self.Attach():
1688
      _ThrowError("drbd%d: can't Attach() in GetSyncStatus", self._aminor)
1689

    
1690
    stats = self.GetProcStatus()
1691
    is_degraded = not stats.is_connected or not stats.is_disk_uptodate
1692

    
1693
    if stats.is_disk_uptodate:
1694
      ldisk_status = constants.LDS_OKAY
1695
    elif stats.is_diskless:
1696
      ldisk_status = constants.LDS_FAULTY
1697
    else:
1698
      ldisk_status = constants.LDS_UNKNOWN
1699

    
1700
    return objects.BlockDevStatus(dev_path=self.dev_path,
1701
                                  major=self.major,
1702
                                  minor=self.minor,
1703
                                  sync_percent=stats.sync_percent,
1704
                                  estimated_time=stats.est_time,
1705
                                  is_degraded=is_degraded,
1706
                                  ldisk_status=ldisk_status)
1707

    
1708
  def Open(self, force=False):
1709
    """Make the local state primary.
1710

1711
    If the 'force' parameter is given, the '-o' option is passed to
1712
    drbdsetup. Since this is a potentially dangerous operation, the
1713
    force flag should be only given after creation, when it actually
1714
    is mandatory.
1715

1716
    """
1717
    if self.minor is None and not self.Attach():
1718
      logging.error("DRBD cannot attach to a device during open")
1719
      return False
1720
    cmd = ["drbdsetup", self.dev_path, "primary"]
1721
    if force:
1722
      cmd.append("-o")
1723
    result = utils.RunCmd(cmd)
1724
    if result.failed:
1725
      _ThrowError("drbd%d: can't make drbd device primary: %s", self.minor,
1726
                  result.output)
1727

    
1728
  def Close(self):
1729
    """Make the local state secondary.
1730

1731
    This will, of course, fail if the device is in use.
1732

1733
    """
1734
    if self.minor is None and not self.Attach():
1735
      _ThrowError("drbd%d: can't Attach() in Close()", self._aminor)
1736
    result = utils.RunCmd(["drbdsetup", self.dev_path, "secondary"])
1737
    if result.failed:
1738
      _ThrowError("drbd%d: can't switch drbd device to secondary: %s",
1739
                  self.minor, result.output)
1740

    
1741
  def DisconnectNet(self):
1742
    """Removes network configuration.
1743

1744
    This method shutdowns the network side of the device.
1745

1746
    The method will wait up to a hardcoded timeout for the device to
1747
    go into standalone after the 'disconnect' command before
1748
    re-configuring it, as sometimes it takes a while for the
1749
    disconnect to actually propagate and thus we might issue a 'net'
1750
    command while the device is still connected. If the device will
1751
    still be attached to the network and we time out, we raise an
1752
    exception.
1753

1754
    """
1755
    if self.minor is None:
1756
      _ThrowError("drbd%d: disk not attached in re-attach net", self._aminor)
1757

    
1758
    if None in (self._lhost, self._lport, self._rhost, self._rport):
1759
      _ThrowError("drbd%d: DRBD disk missing network info in"
1760
                  " DisconnectNet()", self.minor)
1761

    
1762
    class _DisconnectStatus:
1763
      def __init__(self, ever_disconnected):
1764
        self.ever_disconnected = ever_disconnected
1765

    
1766
    dstatus = _DisconnectStatus(_IgnoreError(self._ShutdownNet, self.minor))
1767

    
1768
    def _WaitForDisconnect():
1769
      if self.GetProcStatus().is_standalone:
1770
        return
1771

    
1772
      # retry the disconnect, it seems possible that due to a well-time
1773
      # disconnect on the peer, my disconnect command might be ignored and
1774
      # forgotten
1775
      dstatus.ever_disconnected = \
1776
        _IgnoreError(self._ShutdownNet, self.minor) or dstatus.ever_disconnected
1777

    
1778
      raise utils.RetryAgain()
1779

    
1780
    # Keep start time
1781
    start_time = time.time()
1782

    
1783
    try:
1784
      # Start delay at 100 milliseconds and grow up to 2 seconds
1785
      utils.Retry(_WaitForDisconnect, (0.1, 1.5, 2.0),
1786
                  self._NET_RECONFIG_TIMEOUT)
1787
    except utils.RetryTimeout:
1788
      if dstatus.ever_disconnected:
1789
        msg = ("drbd%d: device did not react to the"
1790
               " 'disconnect' command in a timely manner")
1791
      else:
1792
        msg = "drbd%d: can't shutdown network, even after multiple retries"
1793

    
1794
      _ThrowError(msg, self.minor)
1795

    
1796
    reconfig_time = time.time() - start_time
1797
    if reconfig_time > (self._NET_RECONFIG_TIMEOUT * 0.25):
1798
      logging.info("drbd%d: DisconnectNet: detach took %.3f seconds",
1799
                   self.minor, reconfig_time)
1800

    
1801
  def AttachNet(self, multimaster):
1802
    """Reconnects the network.
1803

1804
    This method connects the network side of the device with a
1805
    specified multi-master flag. The device needs to be 'Standalone'
1806
    but have valid network configuration data.
1807

1808
    Args:
1809
      - multimaster: init the network in dual-primary mode
1810

1811
    """
1812
    if self.minor is None:
1813
      _ThrowError("drbd%d: device not attached in AttachNet", self._aminor)
1814

    
1815
    if None in (self._lhost, self._lport, self._rhost, self._rport):
1816
      _ThrowError("drbd%d: missing network info in AttachNet()", self.minor)
1817

    
1818
    status = self.GetProcStatus()
1819

    
1820
    if not status.is_standalone:
1821
      _ThrowError("drbd%d: device is not standalone in AttachNet", self.minor)
1822

    
1823
    self._AssembleNet(self.minor,
1824
                      (self._lhost, self._lport, self._rhost, self._rport),
1825
                      constants.DRBD_NET_PROTOCOL, dual_pri=multimaster,
1826
                      hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1827

    
1828
  def Attach(self):
1829
    """Check if our minor is configured.
1830

1831
    This doesn't do any device configurations - it only checks if the
1832
    minor is in a state different from Unconfigured.
1833

1834
    Note that this function will not change the state of the system in
1835
    any way (except in case of side-effects caused by reading from
1836
    /proc).
1837

1838
    """
1839
    used_devs = self.GetUsedDevs()
1840
    if self._aminor in used_devs:
1841
      minor = self._aminor
1842
    else:
1843
      minor = None
1844

    
1845
    self._SetFromMinor(minor)
1846
    return minor is not None
1847

    
1848
  def Assemble(self):
1849
    """Assemble the drbd.
1850

1851
    Method:
1852
      - if we have a configured device, we try to ensure that it matches
1853
        our config
1854
      - if not, we create it from zero
1855
      - anyway, set the device parameters
1856

1857
    """
1858
    super(DRBD8, self).Assemble()
1859

    
1860
    self.Attach()
1861
    if self.minor is None:
1862
      # local device completely unconfigured
1863
      self._FastAssemble()
1864
    else:
1865
      # we have to recheck the local and network status and try to fix
1866
      # the device
1867
      self._SlowAssemble()
1868

    
1869
    self.SetSyncParams(self.params)
1870

    
1871
  def _SlowAssemble(self):
1872
    """Assembles the DRBD device from a (partially) configured device.
1873

1874
    In case of partially attached (local device matches but no network
1875
    setup), we perform the network attach. If successful, we re-test
1876
    the attach if can return success.
1877

1878
    """
1879
    # TODO: Rewrite to not use a for loop just because there is 'break'
1880
    # pylint: disable=W0631
1881
    net_data = (self._lhost, self._lport, self._rhost, self._rport)
1882
    for minor in (self._aminor,):
1883
      info = self._GetDevInfo(self._GetShowData(minor))
1884
      match_l = self._MatchesLocal(info)
1885
      match_r = self._MatchesNet(info)
1886

    
1887
      if match_l and match_r:
1888
        # everything matches
1889
        break
1890

    
1891
      if match_l and not match_r and "local_addr" not in info:
1892
        # disk matches, but not attached to network, attach and recheck
1893
        self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
1894
                          hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1895
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1896
          break
1897
        else:
1898
          _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
1899
                      " show' disagrees", minor)
1900

    
1901
      if match_r and "local_dev" not in info:
1902
        # no local disk, but network attached and it matches
1903
        self._AssembleLocal(minor, self._children[0].dev_path,
1904
                            self._children[1].dev_path, self.size)
1905
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1906
          break
1907
        else:
1908
          _ThrowError("drbd%d: disk attach successful, but 'drbdsetup"
1909
                      " show' disagrees", minor)
1910

    
1911
      # this case must be considered only if we actually have local
1912
      # storage, i.e. not in diskless mode, because all diskless
1913
      # devices are equal from the point of view of local
1914
      # configuration
1915
      if (match_l and "local_dev" in info and
1916
          not match_r and "local_addr" in info):
1917
        # strange case - the device network part points to somewhere
1918
        # else, even though its local storage is ours; as we own the
1919
        # drbd space, we try to disconnect from the remote peer and
1920
        # reconnect to our correct one
1921
        try:
1922
          self._ShutdownNet(minor)
1923
        except errors.BlockDeviceError, err:
1924
          _ThrowError("drbd%d: device has correct local storage, wrong"
1925
                      " remote peer and is unable to disconnect in order"
1926
                      " to attach to the correct peer: %s", minor, str(err))
1927
        # note: _AssembleNet also handles the case when we don't want
1928
        # local storage (i.e. one or more of the _[lr](host|port) is
1929
        # None)
1930
        self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
1931
                          hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1932
        if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
1933
          break
1934
        else:
1935
          _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
1936
                      " show' disagrees", minor)
1937

    
1938
    else:
1939
      minor = None
1940

    
1941
    self._SetFromMinor(minor)
1942
    if minor is None:
1943
      _ThrowError("drbd%d: cannot activate, unknown or unhandled reason",
1944
                  self._aminor)
1945

    
1946
  def _FastAssemble(self):
1947
    """Assemble the drbd device from zero.
1948

1949
    This is run when in Assemble we detect our minor is unused.
1950

1951
    """
1952
    minor = self._aminor
1953
    if self._children and self._children[0] and self._children[1]:
1954
      self._AssembleLocal(minor, self._children[0].dev_path,
1955
                          self._children[1].dev_path, self.size)
1956
    if self._lhost and self._lport and self._rhost and self._rport:
1957
      self._AssembleNet(minor,
1958
                        (self._lhost, self._lport, self._rhost, self._rport),
1959
                        constants.DRBD_NET_PROTOCOL,
1960
                        hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1961
    self._SetFromMinor(minor)
1962

    
1963
  @classmethod
1964
  def _ShutdownLocal(cls, minor):
1965
    """Detach from the local device.
1966

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

1970
    """
1971
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "detach"])
1972
    if result.failed:
1973
      _ThrowError("drbd%d: can't detach local disk: %s", minor, result.output)
1974

    
1975
  @classmethod
1976
  def _ShutdownNet(cls, minor):
1977
    """Disconnect from the remote peer.
1978

1979
    This fails if we don't have a local device.
1980

1981
    """
1982
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disconnect"])
1983
    if result.failed:
1984
      _ThrowError("drbd%d: can't shutdown network: %s", minor, result.output)
1985

    
1986
  @classmethod
1987
  def _ShutdownAll(cls, minor):
1988
    """Deactivate the device.
1989

1990
    This will, of course, fail if the device is in use.
1991

1992
    """
1993
    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "down"])
1994
    if result.failed:
1995
      _ThrowError("drbd%d: can't shutdown drbd device: %s",
1996
                  minor, result.output)
1997

    
1998
  def Shutdown(self):
1999
    """Shutdown the DRBD device.
2000

2001
    """
2002
    if self.minor is None and not self.Attach():
2003
      logging.info("drbd%d: not attached during Shutdown()", self._aminor)
2004
      return
2005
    minor = self.minor
2006
    self.minor = None
2007
    self.dev_path = None
2008
    self._ShutdownAll(minor)
2009

    
2010
  def Remove(self):
2011
    """Stub remove for DRBD devices.
2012

2013
    """
2014
    self.Shutdown()
2015

    
2016
  @classmethod
2017
  def Create(cls, unique_id, children, size, params):
2018
    """Create a new DRBD8 device.
2019

2020
    Since DRBD devices are not created per se, just assembled, this
2021
    function only initializes the metadata.
2022

2023
    """
2024
    if len(children) != 2:
2025
      raise errors.ProgrammerError("Invalid setup for the drbd device")
2026
    # check that the minor is unused
2027
    aminor = unique_id[4]
2028
    proc_info = cls._MassageProcData(cls._GetProcData())
2029
    if aminor in proc_info:
2030
      status = DRBD8Status(proc_info[aminor])
2031
      in_use = status.is_in_use
2032
    else:
2033
      in_use = False
2034
    if in_use:
2035
      _ThrowError("drbd%d: minor is already in use at Create() time", aminor)
2036
    meta = children[1]
2037
    meta.Assemble()
2038
    if not meta.Attach():
2039
      _ThrowError("drbd%d: can't attach to meta device '%s'",
2040
                  aminor, meta)
2041
    cls._CheckMetaSize(meta.dev_path)
2042
    cls._InitMeta(aminor, meta.dev_path)
2043
    return cls(unique_id, children, size, params)
2044

    
2045
  def Grow(self, amount, dryrun):
2046
    """Resize the DRBD device and its backing storage.
2047

2048
    """
2049
    if self.minor is None:
2050
      _ThrowError("drbd%d: Grow called while not attached", self._aminor)
2051
    if len(self._children) != 2 or None in self._children:
2052
      _ThrowError("drbd%d: cannot grow diskless device", self.minor)
2053
    self._children[0].Grow(amount, dryrun)
2054
    if dryrun:
2055
      # DRBD does not support dry-run mode, so we'll return here
2056
      return
2057
    result = utils.RunCmd(["drbdsetup", self.dev_path, "resize", "-s",
2058
                           "%dm" % (self.size + amount)])
2059
    if result.failed:
2060
      _ThrowError("drbd%d: resize failed: %s", self.minor, result.output)
2061

    
2062

    
2063
class FileStorage(BlockDev):
2064
  """File device.
2065

2066
  This class represents the a file storage backend device.
2067

2068
  The unique_id for the file device is a (file_driver, file_path) tuple.
2069

2070
  """
2071
  def __init__(self, unique_id, children, size, params):
2072
    """Initalizes a file device backend.
2073

2074
    """
2075
    if children:
2076
      raise errors.BlockDeviceError("Invalid setup for file device")
2077
    super(FileStorage, self).__init__(unique_id, children, size, params)
2078
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
2079
      raise ValueError("Invalid configuration data %s" % str(unique_id))
2080
    self.driver = unique_id[0]
2081
    self.dev_path = unique_id[1]
2082
    self.Attach()
2083

    
2084
  def Assemble(self):
2085
    """Assemble the device.
2086

2087
    Checks whether the file device exists, raises BlockDeviceError otherwise.
2088

2089
    """
2090
    if not os.path.exists(self.dev_path):
2091
      _ThrowError("File device '%s' does not exist" % self.dev_path)
2092

    
2093
  def Shutdown(self):
2094
    """Shutdown the device.
2095

2096
    This is a no-op for the file type, as we don't deactivate
2097
    the file on shutdown.
2098

2099
    """
2100
    pass
2101

    
2102
  def Open(self, force=False):
2103
    """Make the device ready for I/O.
2104

2105
    This is a no-op for the file type.
2106

2107
    """
2108
    pass
2109

    
2110
  def Close(self):
2111
    """Notifies that the device will no longer be used for I/O.
2112

2113
    This is a no-op for the file type.
2114

2115
    """
2116
    pass
2117

    
2118
  def Remove(self):
2119
    """Remove the file backing the block device.
2120

2121
    @rtype: boolean
2122
    @return: True if the removal was successful
2123

2124
    """
2125
    try:
2126
      os.remove(self.dev_path)
2127
    except OSError, err:
2128
      if err.errno != errno.ENOENT:
2129
        _ThrowError("Can't remove file '%s': %s", self.dev_path, err)
2130

    
2131
  def Rename(self, new_id):
2132
    """Renames the file.
2133

2134
    """
2135
    # TODO: implement rename for file-based storage
2136
    _ThrowError("Rename is not supported for file-based storage")
2137

    
2138
  def Grow(self, amount, dryrun):
2139
    """Grow the file
2140

2141
    @param amount: the amount (in mebibytes) to grow with
2142

2143
    """
2144
    # Check that the file exists
2145
    self.Assemble()
2146
    current_size = self.GetActualSize()
2147
    new_size = current_size + amount * 1024 * 1024
2148
    assert new_size > current_size, "Cannot Grow with a negative amount"
2149
    # We can't really simulate the growth
2150
    if dryrun:
2151
      return
2152
    try:
2153
      f = open(self.dev_path, "a+")
2154
      f.truncate(new_size)
2155
      f.close()
2156
    except EnvironmentError, err:
2157
      _ThrowError("Error in file growth: %", str(err))
2158

    
2159
  def Attach(self):
2160
    """Attach to an existing file.
2161

2162
    Check if this file already exists.
2163

2164
    @rtype: boolean
2165
    @return: True if file exists
2166

2167
    """
2168
    self.attached = os.path.exists(self.dev_path)
2169
    return self.attached
2170

    
2171
  def GetActualSize(self):
2172
    """Return the actual disk size.
2173

2174
    @note: the device needs to be active when this is called
2175

2176
    """
2177
    assert self.attached, "BlockDevice not attached in GetActualSize()"
2178
    try:
2179
      st = os.stat(self.dev_path)
2180
      return st.st_size
2181
    except OSError, err:
2182
      _ThrowError("Can't stat %s: %s", self.dev_path, err)
2183

    
2184
  @classmethod
2185
  def Create(cls, unique_id, children, size, params):
2186
    """Create a new file.
2187

2188
    @param size: the size of file in MiB
2189

2190
    @rtype: L{bdev.FileStorage}
2191
    @return: an instance of FileStorage
2192

2193
    """
2194
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
2195
      raise ValueError("Invalid configuration data %s" % str(unique_id))
2196
    dev_path = unique_id[1]
2197
    try:
2198
      fd = os.open(dev_path, os.O_RDWR | os.O_CREAT | os.O_EXCL)
2199
      f = os.fdopen(fd, "w")
2200
      f.truncate(size * 1024 * 1024)
2201
      f.close()
2202
    except EnvironmentError, err:
2203
      if err.errno == errno.EEXIST:
2204
        _ThrowError("File already existing: %s", dev_path)
2205
      _ThrowError("Error in file creation: %", str(err))
2206

    
2207
    return FileStorage(unique_id, children, size, params)
2208

    
2209

    
2210
class PersistentBlockDevice(BlockDev):
2211
  """A block device with persistent node
2212

2213
  May be either directly attached, or exposed through DM (e.g. dm-multipath).
2214
  udev helpers are probably required to give persistent, human-friendly
2215
  names.
2216

2217
  For the time being, pathnames are required to lie under /dev.
2218

2219
  """
2220
  def __init__(self, unique_id, children, size, params):
2221
    """Attaches to a static block device.
2222

2223
    The unique_id is a path under /dev.
2224

2225
    """
2226
    super(PersistentBlockDevice, self).__init__(unique_id, children, size,
2227
                                                params)
2228
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
2229
      raise ValueError("Invalid configuration data %s" % str(unique_id))
2230
    self.dev_path = unique_id[1]
2231
    if not os.path.realpath(self.dev_path).startswith("/dev/"):
2232
      raise ValueError("Full path '%s' lies outside /dev" %
2233
                              os.path.realpath(self.dev_path))
2234
    # TODO: this is just a safety guard checking that we only deal with devices
2235
    # we know how to handle. In the future this will be integrated with
2236
    # external storage backends and possible values will probably be collected
2237
    # from the cluster configuration.
2238
    if unique_id[0] != constants.BLOCKDEV_DRIVER_MANUAL:
2239
      raise ValueError("Got persistent block device of invalid type: %s" %
2240
                       unique_id[0])
2241

    
2242
    self.major = self.minor = None
2243
    self.Attach()
2244

    
2245
  @classmethod
2246
  def Create(cls, unique_id, children, size, params):
2247
    """Create a new device
2248

2249
    This is a noop, we only return a PersistentBlockDevice instance
2250

2251
    """
2252
    return PersistentBlockDevice(unique_id, children, 0, params)
2253

    
2254
  def Remove(self):
2255
    """Remove a device
2256

2257
    This is a noop
2258

2259
    """
2260
    pass
2261

    
2262
  def Rename(self, new_id):
2263
    """Rename this device.
2264

2265
    """
2266
    _ThrowError("Rename is not supported for PersistentBlockDev storage")
2267

    
2268
  def Attach(self):
2269
    """Attach to an existing block device.
2270

2271

2272
    """
2273
    self.attached = False
2274
    try:
2275
      st = os.stat(self.dev_path)
2276
    except OSError, err:
2277
      logging.error("Error stat()'ing %s: %s", self.dev_path, str(err))
2278
      return False
2279

    
2280
    if not stat.S_ISBLK(st.st_mode):
2281
      logging.error("%s is not a block device", self.dev_path)
2282
      return False
2283

    
2284
    self.major = os.major(st.st_rdev)
2285
    self.minor = os.minor(st.st_rdev)
2286
    self.attached = True
2287

    
2288
    return True
2289

    
2290
  def Assemble(self):
2291
    """Assemble the device.
2292

2293
    """
2294
    pass
2295

    
2296
  def Shutdown(self):
2297
    """Shutdown the device.
2298

2299
    """
2300
    pass
2301

    
2302
  def Open(self, force=False):
2303
    """Make the device ready for I/O.
2304

2305
    """
2306
    pass
2307

    
2308
  def Close(self):
2309
    """Notifies that the device will no longer be used for I/O.
2310

2311
    """
2312
    pass
2313

    
2314
  def Grow(self, amount, dryrun):
2315
    """Grow the logical volume.
2316

2317
    """
2318
    _ThrowError("Grow is not supported for PersistentBlockDev storage")
2319

    
2320

    
2321
DEV_MAP = {
2322
  constants.LD_LV: LogicalVolume,
2323
  constants.LD_DRBD8: DRBD8,
2324
  constants.LD_BLOCKDEV: PersistentBlockDevice,
2325
  }
2326

    
2327
if constants.ENABLE_FILE_STORAGE or constants.ENABLE_SHARED_FILE_STORAGE:
2328
  DEV_MAP[constants.LD_FILE] = FileStorage
2329

    
2330

    
2331
def _VerifyDiskType(dev_type):
2332
  if dev_type not in DEV_MAP:
2333
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
2334

    
2335

    
2336
def FindDevice(disk, children):
2337
  """Search for an existing, assembled device.
2338

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

2342
  @type disk: L{objects.Disk}
2343
  @param disk: the disk object to find
2344
  @type children: list of L{bdev.BlockDev}
2345
  @param children: the list of block devices that are children of the device
2346
                  represented by the disk parameter
2347

2348
  """
2349
  _VerifyDiskType(disk.dev_type)
2350
  dev_params = objects.FillDict(constants.DISK_LD_DEFAULTS[disk.dev_type],
2351
                                disk.params)
2352
  device = DEV_MAP[disk.dev_type](disk.physical_id, children, disk.size,
2353
                                  dev_params)
2354
  if not device.attached:
2355
    return None
2356
  return device
2357

    
2358

    
2359
def Assemble(disk, children):
2360
  """Try to attach or assemble an existing device.
2361

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

2365
  @type disk: L{objects.Disk}
2366
  @param disk: the disk object to assemble
2367
  @type children: list of L{bdev.BlockDev}
2368
  @param children: the list of block devices that are children of the device
2369
                  represented by the disk parameter
2370

2371
  """
2372
  _VerifyDiskType(disk.dev_type)
2373
  dev_params = objects.FillDict(constants.DISK_LD_DEFAULTS[disk.dev_type],
2374
                                disk.params)
2375
  device = DEV_MAP[disk.dev_type](disk.physical_id, children, disk.size,
2376
                                  dev_params)
2377
  device.Assemble()
2378
  return device
2379

    
2380

    
2381
def Create(disk, children):
2382
  """Create a device.
2383

2384
  @type disk: L{objects.Disk}
2385
  @param disk: the disk object to create
2386
  @type children: list of L{bdev.BlockDev}
2387
  @param children: the list of block devices that are children of the device
2388
                  represented by the disk parameter
2389

2390
  """
2391
  _VerifyDiskType(disk.dev_type)
2392
  dev_params = objects.FillDict(constants.DISK_LD_DEFAULTS[disk.dev_type],
2393
                                disk.params)
2394
  device = DEV_MAP[disk.dev_type].Create(disk.physical_id, children, disk.size,
2395
                                         dev_params)
2396
  return device