Statistics
| Branch: | Tag: | Revision:

root / lib / storage / bdev.py @ bddc92ee

History | View | Annotate | Download (54.7 kB)

1
#
2
#
3

    
4
# Copyright (C) 2006, 2007, 2010, 2011, 2012, 2013 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
"""
25

    
26
import re
27
import stat
28
import os
29
import logging
30
import math
31

    
32
from ganeti import utils
33
from ganeti import errors
34
from ganeti import constants
35
from ganeti import objects
36
from ganeti import compat
37
from ganeti import pathutils
38
from ganeti import serializer
39
from ganeti.storage import base
40
from ganeti.storage import drbd
41
from ganeti.storage.filestorage import FileStorage
42
from ganeti.storage.gluster import GlusterStorage
43

    
44

    
45
class RbdShowmappedJsonError(Exception):
46
  """`rbd showmmapped' JSON formatting error Exception class.
47

48
  """
49
  pass
50

    
51

    
52
def _CheckResult(result):
53
  """Throws an error if the given result is a failed one.
54

55
  @param result: result from RunCmd
56

57
  """
58
  if result.failed:
59
    base.ThrowError("Command: %s error: %s - %s",
60
                    result.cmd, result.fail_reason, result.output)
61

    
62

    
63
class LogicalVolume(base.BlockDev):
64
  """Logical Volume block device.
65

66
  """
67
  _VALID_NAME_RE = re.compile("^[a-zA-Z0-9+_.-]*$")
68
  _PARSE_PV_DEV_RE = re.compile(r"^([^ ()]+)\([0-9]+\)$")
69
  _INVALID_NAMES = compat.UniqueFrozenset([".", "..", "snapshot", "pvmove"])
70
  _INVALID_SUBSTRINGS = compat.UniqueFrozenset(["_mlog", "_mimage"])
71

    
72
  def __init__(self, unique_id, children, size, params, dyn_params, *args):
73
    """Attaches to a LV device.
74

75
    The unique_id is a tuple (vg_name, lv_name)
76

77
    """
78
    super(LogicalVolume, self).__init__(unique_id, children, size, params,
79
                                        dyn_params, *args)
80
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
81
      raise ValueError("Invalid configuration data %s" % str(unique_id))
82
    self._vg_name, self._lv_name = unique_id
83
    self._ValidateName(self._vg_name)
84
    self._ValidateName(self._lv_name)
85
    self.dev_path = utils.PathJoin("/dev", self._vg_name, self._lv_name)
86
    self._degraded = True
87
    self.major = self.minor = self.pe_size = self.stripe_count = None
88
    self.pv_names = None
89
    self.Attach()
90

    
91
  @staticmethod
92
  def _GetStdPvSize(pvs_info):
93
    """Return the the standard PV size (used with exclusive storage).
94

95
    @param pvs_info: list of objects.LvmPvInfo, cannot be empty
96
    @rtype: float
97
    @return: size in MiB
98

99
    """
100
    assert len(pvs_info) > 0
101
    smallest = min([pv.size for pv in pvs_info])
102
    return smallest / (1 + constants.PART_MARGIN + constants.PART_RESERVED)
103

    
104
  @staticmethod
105
  def _ComputeNumPvs(size, pvs_info):
106
    """Compute the number of PVs needed for an LV (with exclusive storage).
107

108
    @type size: float
109
    @param size: LV size in MiB
110
    @param pvs_info: list of objects.LvmPvInfo, cannot be empty
111
    @rtype: integer
112
    @return: number of PVs needed
113
    """
114
    assert len(pvs_info) > 0
115
    pv_size = float(LogicalVolume._GetStdPvSize(pvs_info))
116
    return int(math.ceil(float(size) / pv_size))
117

    
118
  @staticmethod
119
  def _GetEmptyPvNames(pvs_info, max_pvs=None):
120
    """Return a list of empty PVs, by name.
121

122
    """
123
    empty_pvs = filter(objects.LvmPvInfo.IsEmpty, pvs_info)
124
    if max_pvs is not None:
125
      empty_pvs = empty_pvs[:max_pvs]
126
    return map((lambda pv: pv.name), empty_pvs)
127

    
128
  @classmethod
129
  def Create(cls, unique_id, children, size, spindles, params, excl_stor,
130
             dyn_params, *args):
131
    """Create a new logical volume.
132

133
    """
134
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
135
      raise errors.ProgrammerError("Invalid configuration data %s" %
136
                                   str(unique_id))
137
    vg_name, lv_name = unique_id
138
    cls._ValidateName(vg_name)
139
    cls._ValidateName(lv_name)
140
    pvs_info = cls.GetPVInfo([vg_name])
141
    if not pvs_info:
142
      if excl_stor:
143
        msg = "No (empty) PVs found"
144
      else:
145
        msg = "Can't compute PV info for vg %s" % vg_name
146
      base.ThrowError(msg)
147
    pvs_info.sort(key=(lambda pv: pv.free), reverse=True)
148

    
149
    pvlist = [pv.name for pv in pvs_info]
150
    if compat.any(":" in v for v in pvlist):
151
      base.ThrowError("Some of your PVs have the invalid character ':' in their"
152
                      " name, this is not supported - please filter them out"
153
                      " in lvm.conf using either 'filter' or 'preferred_names'")
154

    
155
    current_pvs = len(pvlist)
156
    desired_stripes = params[constants.LDP_STRIPES]
157
    stripes = min(current_pvs, desired_stripes)
158

    
159
    if excl_stor:
160
      if spindles is None:
161
        base.ThrowError("Unspecified number of spindles: this is required"
162
                        "when exclusive storage is enabled, try running"
163
                        " gnt-cluster repair-disk-sizes")
164
      (err_msgs, _) = utils.LvmExclusiveCheckNodePvs(pvs_info)
165
      if err_msgs:
166
        for m in err_msgs:
167
          logging.warning(m)
168
      req_pvs = cls._ComputeNumPvs(size, pvs_info)
169
      if spindles < req_pvs:
170
        base.ThrowError("Requested number of spindles (%s) is not enough for"
171
                        " a disk of %d MB (at least %d spindles needed)",
172
                        spindles, size, req_pvs)
173
      else:
174
        req_pvs = spindles
175
      pvlist = cls._GetEmptyPvNames(pvs_info, req_pvs)
176
      current_pvs = len(pvlist)
177
      if current_pvs < req_pvs:
178
        base.ThrowError("Not enough empty PVs (spindles) to create a disk of %d"
179
                        " MB: %d available, %d needed",
180
                        size, current_pvs, req_pvs)
181
      assert current_pvs == len(pvlist)
182
      # We must update stripes to be sure to use all the desired spindles
183
      stripes = current_pvs
184
      if stripes > desired_stripes:
185
        # Don't warn when lowering stripes, as it's no surprise
186
        logging.warning("Using %s stripes instead of %s, to be able to use"
187
                        " %s spindles", stripes, desired_stripes, current_pvs)
188

    
189
    else:
190
      if stripes < desired_stripes:
191
        logging.warning("Could not use %d stripes for VG %s, as only %d PVs are"
192
                        " available.", desired_stripes, vg_name, current_pvs)
193
      free_size = sum([pv.free for pv in pvs_info])
194
      # The size constraint should have been checked from the master before
195
      # calling the create function.
196
      if free_size < size:
197
        base.ThrowError("Not enough free space: required %s,"
198
                        " available %s", size, free_size)
199

    
200
    # If the free space is not well distributed, we won't be able to
201
    # create an optimally-striped volume; in that case, we want to try
202
    # with N, N-1, ..., 2, and finally 1 (non-stripped) number of
203
    # stripes
204
    cmd = ["lvcreate", "-L%dm" % size, "-n%s" % lv_name]
205
    for stripes_arg in range(stripes, 0, -1):
206
      result = utils.RunCmd(cmd + ["-i%d" % stripes_arg] + [vg_name] + pvlist)
207
      if not result.failed:
208
        break
209
    if result.failed:
210
      base.ThrowError("LV create failed (%s): %s",
211
                      result.fail_reason, result.output)
212
    return LogicalVolume(unique_id, children, size, params, dyn_params, *args)
213

    
214
  @staticmethod
215
  def _GetVolumeInfo(lvm_cmd, fields):
216
    """Returns LVM Volume infos using lvm_cmd
217

218
    @param lvm_cmd: Should be one of "pvs", "vgs" or "lvs"
219
    @param fields: Fields to return
220
    @return: A list of dicts each with the parsed fields
221

222
    """
223
    if not fields:
224
      raise errors.ProgrammerError("No fields specified")
225

    
226
    sep = "|"
227
    cmd = [lvm_cmd, "--noheadings", "--nosuffix", "--units=m", "--unbuffered",
228
           "--separator=%s" % sep, "-o%s" % ",".join(fields)]
229

    
230
    result = utils.RunCmd(cmd)
231
    if result.failed:
232
      raise errors.CommandError("Can't get the volume information: %s - %s" %
233
                                (result.fail_reason, result.output))
234

    
235
    data = []
236
    for line in result.stdout.splitlines():
237
      splitted_fields = line.strip().split(sep)
238

    
239
      if len(fields) != len(splitted_fields):
240
        raise errors.CommandError("Can't parse %s output: line '%s'" %
241
                                  (lvm_cmd, line))
242

    
243
      data.append(splitted_fields)
244

    
245
    return data
246

    
247
  @classmethod
248
  def GetPVInfo(cls, vg_names, filter_allocatable=True, include_lvs=False):
249
    """Get the free space info for PVs in a volume group.
250

251
    @param vg_names: list of volume group names, if empty all will be returned
252
    @param filter_allocatable: whether to skip over unallocatable PVs
253
    @param include_lvs: whether to include a list of LVs hosted on each PV
254

255
    @rtype: list
256
    @return: list of objects.LvmPvInfo objects
257

258
    """
259
    # We request "lv_name" field only if we care about LVs, so we don't get
260
    # a long list of entries with many duplicates unless we really have to.
261
    # The duplicate "pv_name" field will be ignored.
262
    if include_lvs:
263
      lvfield = "lv_name"
264
    else:
265
      lvfield = "pv_name"
266
    try:
267
      info = cls._GetVolumeInfo("pvs", ["pv_name", "vg_name", "pv_free",
268
                                        "pv_attr", "pv_size", lvfield])
269
    except errors.GenericError, err:
270
      logging.error("Can't get PV information: %s", err)
271
      return None
272

    
273
    # When asked for LVs, "pvs" may return multiple entries for the same PV-LV
274
    # pair. We sort entries by PV name and then LV name, so it's easy to weed
275
    # out duplicates.
276
    if include_lvs:
277
      info.sort(key=(lambda i: (i[0], i[5])))
278
    data = []
279
    lastpvi = None
280
    for (pv_name, vg_name, pv_free, pv_attr, pv_size, lv_name) in info:
281
      # (possibly) skip over pvs which are not allocatable
282
      if filter_allocatable and pv_attr[0] != "a":
283
        continue
284
      # (possibly) skip over pvs which are not in the right volume group(s)
285
      if vg_names and vg_name not in vg_names:
286
        continue
287
      # Beware of duplicates (check before inserting)
288
      if lastpvi and lastpvi.name == pv_name:
289
        if include_lvs and lv_name:
290
          if not lastpvi.lv_list or lastpvi.lv_list[-1] != lv_name:
291
            lastpvi.lv_list.append(lv_name)
292
      else:
293
        if include_lvs and lv_name:
294
          lvl = [lv_name]
295
        else:
296
          lvl = []
297
        lastpvi = objects.LvmPvInfo(name=pv_name, vg_name=vg_name,
298
                                    size=float(pv_size), free=float(pv_free),
299
                                    attributes=pv_attr, lv_list=lvl)
300
        data.append(lastpvi)
301

    
302
    return data
303

    
304
  @classmethod
305
  def _GetRawFreePvInfo(cls, vg_name):
306
    """Return info (size/free) about PVs.
307

308
    @type vg_name: string
309
    @param vg_name: VG name
310
    @rtype: tuple
311
    @return: (standard_pv_size_in_MiB, number_of_free_pvs, total_number_of_pvs)
312

313
    """
314
    pvs_info = cls.GetPVInfo([vg_name])
315
    if not pvs_info:
316
      pv_size = 0.0
317
      free_pvs = 0
318
      num_pvs = 0
319
    else:
320
      pv_size = cls._GetStdPvSize(pvs_info)
321
      free_pvs = len(cls._GetEmptyPvNames(pvs_info))
322
      num_pvs = len(pvs_info)
323
    return (pv_size, free_pvs, num_pvs)
324

    
325
  @classmethod
326
  def _GetExclusiveStorageVgFree(cls, vg_name):
327
    """Return the free disk space in the given VG, in exclusive storage mode.
328

329
    @type vg_name: string
330
    @param vg_name: VG name
331
    @rtype: float
332
    @return: free space in MiB
333
    """
334
    (pv_size, free_pvs, _) = cls._GetRawFreePvInfo(vg_name)
335
    return pv_size * free_pvs
336

    
337
  @classmethod
338
  def GetVgSpindlesInfo(cls, vg_name):
339
    """Get the free space info for specific VGs.
340

341
    @param vg_name: volume group name
342
    @rtype: tuple
343
    @return: (free_spindles, total_spindles)
344

345
    """
346
    (_, free_pvs, num_pvs) = cls._GetRawFreePvInfo(vg_name)
347
    return (free_pvs, num_pvs)
348

    
349
  @classmethod
350
  def GetVGInfo(cls, vg_names, excl_stor, filter_readonly=True):
351
    """Get the free space info for specific VGs.
352

353
    @param vg_names: list of volume group names, if empty all will be returned
354
    @param excl_stor: whether exclusive_storage is enabled
355
    @param filter_readonly: whether to skip over readonly VGs
356

357
    @rtype: list
358
    @return: list of tuples (free_space, total_size, name) with free_space in
359
             MiB
360

361
    """
362
    try:
363
      info = cls._GetVolumeInfo("vgs", ["vg_name", "vg_free", "vg_attr",
364
                                        "vg_size"])
365
    except errors.GenericError, err:
366
      logging.error("Can't get VG information: %s", err)
367
      return None
368

    
369
    data = []
370
    for vg_name, vg_free, vg_attr, vg_size in info:
371
      # (possibly) skip over vgs which are not writable
372
      if filter_readonly and vg_attr[0] == "r":
373
        continue
374
      # (possibly) skip over vgs which are not in the right volume group(s)
375
      if vg_names and vg_name not in vg_names:
376
        continue
377
      # Exclusive storage needs a different concept of free space
378
      if excl_stor:
379
        es_free = cls._GetExclusiveStorageVgFree(vg_name)
380
        assert es_free <= vg_free
381
        vg_free = es_free
382
      data.append((float(vg_free), float(vg_size), vg_name))
383

    
384
    return data
385

    
386
  @classmethod
387
  def _ValidateName(cls, name):
388
    """Validates that a given name is valid as VG or LV name.
389

390
    The list of valid characters and restricted names is taken out of
391
    the lvm(8) manpage, with the simplification that we enforce both
392
    VG and LV restrictions on the names.
393

394
    """
395
    if (not cls._VALID_NAME_RE.match(name) or
396
        name in cls._INVALID_NAMES or
397
        compat.any(substring in name for substring in cls._INVALID_SUBSTRINGS)):
398
      base.ThrowError("Invalid LVM name '%s'", name)
399

    
400
  def Remove(self):
401
    """Remove this logical volume.
402

403
    """
404
    if not self.minor and not self.Attach():
405
      # the LV does not exist
406
      return
407
    result = utils.RunCmd(["lvremove", "-f", "%s/%s" %
408
                           (self._vg_name, self._lv_name)])
409
    if result.failed:
410
      base.ThrowError("Can't lvremove: %s - %s",
411
                      result.fail_reason, result.output)
412

    
413
  def Rename(self, new_id):
414
    """Rename this logical volume.
415

416
    """
417
    if not isinstance(new_id, (tuple, list)) or len(new_id) != 2:
418
      raise errors.ProgrammerError("Invalid new logical id '%s'" % new_id)
419
    new_vg, new_name = new_id
420
    if new_vg != self._vg_name:
421
      raise errors.ProgrammerError("Can't move a logical volume across"
422
                                   " volume groups (from %s to to %s)" %
423
                                   (self._vg_name, new_vg))
424
    result = utils.RunCmd(["lvrename", new_vg, self._lv_name, new_name])
425
    if result.failed:
426
      base.ThrowError("Failed to rename the logical volume: %s", result.output)
427
    self._lv_name = new_name
428
    self.dev_path = utils.PathJoin("/dev", self._vg_name, self._lv_name)
429

    
430
  @classmethod
431
  def _ParseLvInfoLine(cls, line, sep):
432
    """Parse one line of the lvs output used in L{_GetLvInfo}.
433

434
    """
435
    elems = line.strip().rstrip(sep).split(sep)
436
    if len(elems) != 6:
437
      base.ThrowError("Can't parse LVS output, len(%s) != 6", str(elems))
438

    
439
    (status, major, minor, pe_size, stripes, pvs) = elems
440
    if len(status) < 6:
441
      base.ThrowError("lvs lv_attr is not at least 6 characters (%s)", status)
442

    
443
    try:
444
      major = int(major)
445
      minor = int(minor)
446
    except (TypeError, ValueError), err:
447
      base.ThrowError("lvs major/minor cannot be parsed: %s", str(err))
448

    
449
    try:
450
      pe_size = int(float(pe_size))
451
    except (TypeError, ValueError), err:
452
      base.ThrowError("Can't parse vg extent size: %s", err)
453

    
454
    try:
455
      stripes = int(stripes)
456
    except (TypeError, ValueError), err:
457
      base.ThrowError("Can't parse the number of stripes: %s", err)
458

    
459
    pv_names = []
460
    for pv in pvs.split(","):
461
      m = re.match(cls._PARSE_PV_DEV_RE, pv)
462
      if not m:
463
        base.ThrowError("Can't parse this device list: %s", pvs)
464
      pv_names.append(m.group(1))
465
    assert len(pv_names) > 0
466

    
467
    return (status, major, minor, pe_size, stripes, pv_names)
468

    
469
  @classmethod
470
  def _GetLvInfo(cls, dev_path, _run_cmd=utils.RunCmd):
471
    """Get info about the given existing LV to be used.
472

473
    """
474
    sep = "|"
475
    result = _run_cmd(["lvs", "--noheadings", "--separator=%s" % sep,
476
                       "--units=k", "--nosuffix",
477
                       "-olv_attr,lv_kernel_major,lv_kernel_minor,"
478
                       "vg_extent_size,stripes,devices", dev_path])
479
    if result.failed:
480
      base.ThrowError("Can't find LV %s: %s, %s",
481
                      dev_path, result.fail_reason, result.output)
482
    # the output can (and will) have multiple lines for multi-segment
483
    # LVs, as the 'stripes' parameter is a segment one, so we take
484
    # only the last entry, which is the one we're interested in; note
485
    # that with LVM2 anyway the 'stripes' value must be constant
486
    # across segments, so this is a no-op actually
487
    out = result.stdout.splitlines()
488
    if not out: # totally empty result? splitlines() returns at least
489
                # one line for any non-empty string
490
      base.ThrowError("Can't parse LVS output, no lines? Got '%s'", str(out))
491
    pv_names = set()
492
    for line in out:
493
      (status, major, minor, pe_size, stripes, more_pvs) = \
494
        cls._ParseLvInfoLine(line, sep)
495
      pv_names.update(more_pvs)
496
    return (status, major, minor, pe_size, stripes, pv_names)
497

    
498
  def Attach(self):
499
    """Attach to an existing LV.
500

501
    This method will try to see if an existing and active LV exists
502
    which matches our name. If so, its major/minor will be
503
    recorded.
504

505
    """
506
    self.attached = False
507
    try:
508
      (status, major, minor, pe_size, stripes, pv_names) = \
509
        self._GetLvInfo(self.dev_path)
510
    except errors.BlockDeviceError:
511
      return False
512

    
513
    self.major = major
514
    self.minor = minor
515
    self.pe_size = pe_size
516
    self.stripe_count = stripes
517
    self._degraded = status[0] == "v" # virtual volume, i.e. doesn't backing
518
                                      # storage
519
    self.pv_names = pv_names
520
    self.attached = True
521
    return True
522

    
523
  def Assemble(self):
524
    """Assemble the device.
525

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

530
    """
531
    result = utils.RunCmd(["lvchange", "-ay", self.dev_path])
532
    if result.failed:
533
      base.ThrowError("Can't activate lv %s: %s", self.dev_path, result.output)
534

    
535
  def Shutdown(self):
536
    """Shutdown the device.
537

538
    This is a no-op for the LV device type, as we don't deactivate the
539
    volumes on shutdown.
540

541
    """
542
    pass
543

    
544
  def GetSyncStatus(self):
545
    """Returns the sync status of the device.
546

547
    If this device is a mirroring device, this function returns the
548
    status of the mirror.
549

550
    For logical volumes, sync_percent and estimated_time are always
551
    None (no recovery in progress, as we don't handle the mirrored LV
552
    case). The is_degraded parameter is the inverse of the ldisk
553
    parameter.
554

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

561
    The status was already read in Attach, so we just return it.
562

563
    @rtype: objects.BlockDevStatus
564

565
    """
566
    if self._degraded:
567
      ldisk_status = constants.LDS_FAULTY
568
    else:
569
      ldisk_status = constants.LDS_OKAY
570

    
571
    return objects.BlockDevStatus(dev_path=self.dev_path,
572
                                  major=self.major,
573
                                  minor=self.minor,
574
                                  sync_percent=None,
575
                                  estimated_time=None,
576
                                  is_degraded=self._degraded,
577
                                  ldisk_status=ldisk_status)
578

    
579
  def Open(self, force=False):
580
    """Make the device ready for I/O.
581

582
    This is a no-op for the LV device type.
583

584
    """
585
    pass
586

    
587
  def Close(self):
588
    """Notifies that the device will no longer be used for I/O.
589

590
    This is a no-op for the LV device type.
591

592
    """
593
    pass
594

    
595
  def Snapshot(self, size):
596
    """Create a snapshot copy of an lvm block device.
597

598
    @returns: tuple (vg, lv)
599

600
    """
601
    snap_name = self._lv_name + ".snap"
602

    
603
    # remove existing snapshot if found
604
    snap = LogicalVolume((self._vg_name, snap_name), None, size, self.params,
605
                         self.dyn_params)
606
    base.IgnoreError(snap.Remove)
607

    
608
    vg_info = self.GetVGInfo([self._vg_name], False)
609
    if not vg_info:
610
      base.ThrowError("Can't compute VG info for vg %s", self._vg_name)
611
    free_size, _, _ = vg_info[0]
612
    if free_size < size:
613
      base.ThrowError("Not enough free space: required %s,"
614
                      " available %s", size, free_size)
615

    
616
    _CheckResult(utils.RunCmd(["lvcreate", "-L%dm" % size, "-s",
617
                               "-n%s" % snap_name, self.dev_path]))
618

    
619
    return (self._vg_name, snap_name)
620

    
621
  def _RemoveOldInfo(self):
622
    """Try to remove old tags from the lv.
623

624
    """
625
    result = utils.RunCmd(["lvs", "-o", "tags", "--noheadings", "--nosuffix",
626
                           self.dev_path])
627
    _CheckResult(result)
628

    
629
    raw_tags = result.stdout.strip()
630
    if raw_tags:
631
      for tag in raw_tags.split(","):
632
        _CheckResult(utils.RunCmd(["lvchange", "--deltag",
633
                                   tag.strip(), self.dev_path]))
634

    
635
  def SetInfo(self, text):
636
    """Update metadata with info text.
637

638
    """
639
    base.BlockDev.SetInfo(self, text)
640

    
641
    self._RemoveOldInfo()
642

    
643
    # Replace invalid characters
644
    text = re.sub("^[^A-Za-z0-9_+.]", "_", text)
645
    text = re.sub("[^-A-Za-z0-9_+.]", "_", text)
646

    
647
    # Only up to 128 characters are allowed
648
    text = text[:128]
649

    
650
    _CheckResult(utils.RunCmd(["lvchange", "--addtag", text, self.dev_path]))
651

    
652
  def _GetGrowthAvaliabilityExclStor(self):
653
    """Return how much the disk can grow with exclusive storage.
654

655
    @rtype: float
656
    @return: available space in Mib
657

658
    """
659
    pvs_info = self.GetPVInfo([self._vg_name])
660
    if not pvs_info:
661
      base.ThrowError("Cannot get information about PVs for %s", self.dev_path)
662
    std_pv_size = self._GetStdPvSize(pvs_info)
663
    free_space = sum(pvi.free - (pvi.size - std_pv_size)
664
                        for pvi in pvs_info
665
                        if pvi.name in self.pv_names)
666
    return free_space
667

    
668
  def Grow(self, amount, dryrun, backingstore, excl_stor):
669
    """Grow the logical volume.
670

671
    """
672
    if not backingstore:
673
      return
674
    if self.pe_size is None or self.stripe_count is None:
675
      if not self.Attach():
676
        base.ThrowError("Can't attach to LV during Grow()")
677
    full_stripe_size = self.pe_size * self.stripe_count
678
    # pe_size is in KB
679
    amount *= 1024
680
    rest = amount % full_stripe_size
681
    if rest != 0:
682
      amount += full_stripe_size - rest
683
    cmd = ["lvextend", "-L", "+%dk" % amount]
684
    if dryrun:
685
      cmd.append("--test")
686
    if excl_stor:
687
      free_space = self._GetGrowthAvaliabilityExclStor()
688
      # amount is in KiB, free_space in MiB
689
      if amount > free_space * 1024:
690
        base.ThrowError("Not enough free space to grow %s: %d MiB required,"
691
                        " %d available", self.dev_path, amount / 1024,
692
                        free_space)
693
      # Disk growth doesn't grow the number of spindles, so we must stay within
694
      # our assigned volumes
695
      pvlist = list(self.pv_names)
696
    else:
697
      pvlist = []
698
    # we try multiple algorithms since the 'best' ones might not have
699
    # space available in the right place, but later ones might (since
700
    # they have less constraints); also note that only recent LVM
701
    # supports 'cling'
702
    for alloc_policy in "contiguous", "cling", "normal":
703
      result = utils.RunCmd(cmd + ["--alloc", alloc_policy, self.dev_path] +
704
                            pvlist)
705
      if not result.failed:
706
        return
707
    base.ThrowError("Can't grow LV %s: %s", self.dev_path, result.output)
708

    
709
  def GetActualSpindles(self):
710
    """Return the number of spindles used.
711

712
    """
713
    assert self.attached, "BlockDevice not attached in GetActualSpindles()"
714
    return len(self.pv_names)
715

    
716

    
717
class PersistentBlockDevice(base.BlockDev):
718
  """A block device with persistent node
719

720
  May be either directly attached, or exposed through DM (e.g. dm-multipath).
721
  udev helpers are probably required to give persistent, human-friendly
722
  names.
723

724
  For the time being, pathnames are required to lie under /dev.
725

726
  """
727
  def __init__(self, unique_id, children, size, params, dyn_params, *args):
728
    """Attaches to a static block device.
729

730
    The unique_id is a path under /dev.
731

732
    """
733
    super(PersistentBlockDevice, self).__init__(unique_id, children, size,
734
                                                params, dyn_params, *args)
735
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
736
      raise ValueError("Invalid configuration data %s" % str(unique_id))
737
    self.dev_path = unique_id[1]
738
    if not os.path.realpath(self.dev_path).startswith("/dev/"):
739
      raise ValueError("Full path '%s' lies outside /dev" %
740
                              os.path.realpath(self.dev_path))
741
    # TODO: this is just a safety guard checking that we only deal with devices
742
    # we know how to handle. In the future this will be integrated with
743
    # external storage backends and possible values will probably be collected
744
    # from the cluster configuration.
745
    if unique_id[0] != constants.BLOCKDEV_DRIVER_MANUAL:
746
      raise ValueError("Got persistent block device of invalid type: %s" %
747
                       unique_id[0])
748

    
749
    self.major = self.minor = None
750
    self.Attach()
751

    
752
  @classmethod
753
  def Create(cls, unique_id, children, size, spindles, params, excl_stor,
754
             dyn_params, *args):
755
    """Create a new device
756

757
    This is a noop, we only return a PersistentBlockDevice instance
758

759
    """
760
    if excl_stor:
761
      raise errors.ProgrammerError("Persistent block device requested with"
762
                                   " exclusive_storage")
763
    return PersistentBlockDevice(unique_id, children, 0, params, dyn_params,
764
                                 *args)
765

    
766
  def Remove(self):
767
    """Remove a device
768

769
    This is a noop
770

771
    """
772
    pass
773

    
774
  def Rename(self, new_id):
775
    """Rename this device.
776

777
    """
778
    base.ThrowError("Rename is not supported for PersistentBlockDev storage")
779

    
780
  def Attach(self):
781
    """Attach to an existing block device.
782

783

784
    """
785
    self.attached = False
786
    try:
787
      st = os.stat(self.dev_path)
788
    except OSError, err:
789
      logging.error("Error stat()'ing %s: %s", self.dev_path, str(err))
790
      return False
791

    
792
    if not stat.S_ISBLK(st.st_mode):
793
      logging.error("%s is not a block device", self.dev_path)
794
      return False
795

    
796
    self.major = os.major(st.st_rdev)
797
    self.minor = os.minor(st.st_rdev)
798
    self.attached = True
799

    
800
    return True
801

    
802
  def Assemble(self):
803
    """Assemble the device.
804

805
    """
806
    pass
807

    
808
  def Shutdown(self):
809
    """Shutdown the device.
810

811
    """
812
    pass
813

    
814
  def Open(self, force=False):
815
    """Make the device ready for I/O.
816

817
    """
818
    pass
819

    
820
  def Close(self):
821
    """Notifies that the device will no longer be used for I/O.
822

823
    """
824
    pass
825

    
826
  def Grow(self, amount, dryrun, backingstore, excl_stor):
827
    """Grow the logical volume.
828

829
    """
830
    base.ThrowError("Grow is not supported for PersistentBlockDev storage")
831

    
832

    
833
class RADOSBlockDevice(base.BlockDev):
834
  """A RADOS Block Device (rbd).
835

836
  This class implements the RADOS Block Device for the backend. You need
837
  the rbd kernel driver, the RADOS Tools and a working RADOS cluster for
838
  this to be functional.
839

840
  """
841
  def __init__(self, unique_id, children, size, params, dyn_params, *args):
842
    """Attaches to an rbd device.
843

844
    """
845
    super(RADOSBlockDevice, self).__init__(unique_id, children, size, params,
846
                                           dyn_params, *args)
847
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
848
      raise ValueError("Invalid configuration data %s" % str(unique_id))
849

    
850
    self.driver, self.rbd_name = unique_id
851
    self.rbd_pool = params[constants.LDP_POOL]
852

    
853
    self.major = self.minor = None
854
    self.Attach()
855

    
856
  @classmethod
857
  def Create(cls, unique_id, children, size, spindles, params, excl_stor,
858
             dyn_params, *args):
859
    """Create a new rbd device.
860

861
    Provision a new rbd volume inside a RADOS pool.
862

863
    """
864
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
865
      raise errors.ProgrammerError("Invalid configuration data %s" %
866
                                   str(unique_id))
867
    if excl_stor:
868
      raise errors.ProgrammerError("RBD device requested with"
869
                                   " exclusive_storage")
870
    rbd_pool = params[constants.LDP_POOL]
871
    rbd_name = unique_id[1]
872

    
873
    # Provision a new rbd volume (Image) inside the RADOS cluster.
874
    cmd = [constants.RBD_CMD, "create", "-p", rbd_pool,
875
           rbd_name, "--size", "%s" % size]
876
    result = utils.RunCmd(cmd)
877
    if result.failed:
878
      base.ThrowError("rbd creation failed (%s): %s",
879
                      result.fail_reason, result.output)
880

    
881
    return RADOSBlockDevice(unique_id, children, size, params, dyn_params,
882
                            *args)
883

    
884
  def Remove(self):
885
    """Remove the rbd device.
886

887
    """
888
    rbd_pool = self.params[constants.LDP_POOL]
889
    rbd_name = self.unique_id[1]
890

    
891
    if not self.minor and not self.Attach():
892
      # The rbd device doesn't exist.
893
      return
894

    
895
    # First shutdown the device (remove mappings).
896
    self.Shutdown()
897

    
898
    # Remove the actual Volume (Image) from the RADOS cluster.
899
    cmd = [constants.RBD_CMD, "rm", "-p", rbd_pool, rbd_name]
900
    result = utils.RunCmd(cmd)
901
    if result.failed:
902
      base.ThrowError("Can't remove Volume from cluster with rbd rm: %s - %s",
903
                      result.fail_reason, result.output)
904

    
905
  def Rename(self, new_id):
906
    """Rename this device.
907

908
    """
909
    pass
910

    
911
  def Attach(self):
912
    """Attach to an existing rbd device.
913

914
    This method maps the rbd volume that matches our name with
915
    an rbd device and then attaches to this device.
916

917
    """
918
    self.attached = False
919

    
920
    # Map the rbd volume to a block device under /dev
921
    self.dev_path = self._MapVolumeToBlockdev(self.unique_id)
922

    
923
    try:
924
      st = os.stat(self.dev_path)
925
    except OSError, err:
926
      logging.error("Error stat()'ing %s: %s", self.dev_path, str(err))
927
      return False
928

    
929
    if not stat.S_ISBLK(st.st_mode):
930
      logging.error("%s is not a block device", self.dev_path)
931
      return False
932

    
933
    self.major = os.major(st.st_rdev)
934
    self.minor = os.minor(st.st_rdev)
935
    self.attached = True
936

    
937
    return True
938

    
939
  def _MapVolumeToBlockdev(self, unique_id):
940
    """Maps existing rbd volumes to block devices.
941

942
    This method should be idempotent if the mapping already exists.
943

944
    @rtype: string
945
    @return: the block device path that corresponds to the volume
946

947
    """
948
    pool = self.params[constants.LDP_POOL]
949
    name = unique_id[1]
950

    
951
    # Check if the mapping already exists.
952
    rbd_dev = self._VolumeToBlockdev(pool, name)
953
    if rbd_dev:
954
      # The mapping exists. Return it.
955
      return rbd_dev
956

    
957
    # The mapping doesn't exist. Create it.
958
    map_cmd = [constants.RBD_CMD, "map", "-p", pool, name]
959
    result = utils.RunCmd(map_cmd)
960
    if result.failed:
961
      base.ThrowError("rbd map failed (%s): %s",
962
                      result.fail_reason, result.output)
963

    
964
    # Find the corresponding rbd device.
965
    rbd_dev = self._VolumeToBlockdev(pool, name)
966
    if not rbd_dev:
967
      base.ThrowError("rbd map succeeded, but could not find the rbd block"
968
                      " device in output of showmapped, for volume: %s", name)
969

    
970
    # The device was successfully mapped. Return it.
971
    return rbd_dev
972

    
973
  @classmethod
974
  def _VolumeToBlockdev(cls, pool, volume_name):
975
    """Do the 'volume name'-to-'rbd block device' resolving.
976

977
    @type pool: string
978
    @param pool: RADOS pool to use
979
    @type volume_name: string
980
    @param volume_name: the name of the volume whose device we search for
981
    @rtype: string or None
982
    @return: block device path if the volume is mapped, else None
983

984
    """
985
    try:
986
      # Newer versions of the rbd tool support json output formatting. Use it
987
      # if available.
988
      showmap_cmd = [
989
        constants.RBD_CMD,
990
        "showmapped",
991
        "-p",
992
        pool,
993
        "--format",
994
        "json"
995
        ]
996
      result = utils.RunCmd(showmap_cmd)
997
      if result.failed:
998
        logging.error("rbd JSON output formatting returned error (%s): %s,"
999
                      "falling back to plain output parsing",
1000
                      result.fail_reason, result.output)
1001
        raise RbdShowmappedJsonError
1002

    
1003
      return cls._ParseRbdShowmappedJson(result.output, volume_name)
1004
    except RbdShowmappedJsonError:
1005
      # For older versions of rbd, we have to parse the plain / text output
1006
      # manually.
1007
      showmap_cmd = [constants.RBD_CMD, "showmapped", "-p", pool]
1008
      result = utils.RunCmd(showmap_cmd)
1009
      if result.failed:
1010
        base.ThrowError("rbd showmapped failed (%s): %s",
1011
                        result.fail_reason, result.output)
1012

    
1013
      return cls._ParseRbdShowmappedPlain(result.output, volume_name)
1014

    
1015
  @staticmethod
1016
  def _ParseRbdShowmappedJson(output, volume_name):
1017
    """Parse the json output of `rbd showmapped'.
1018

1019
    This method parses the json output of `rbd showmapped' and returns the rbd
1020
    block device path (e.g. /dev/rbd0) that matches the given rbd volume.
1021

1022
    @type output: string
1023
    @param output: the json output of `rbd showmapped'
1024
    @type volume_name: string
1025
    @param volume_name: the name of the volume whose device we search for
1026
    @rtype: string or None
1027
    @return: block device path if the volume is mapped, else None
1028

1029
    """
1030
    try:
1031
      devices = serializer.LoadJson(output)
1032
    except ValueError, err:
1033
      base.ThrowError("Unable to parse JSON data: %s" % err)
1034

    
1035
    rbd_dev = None
1036
    for d in devices.values(): # pylint: disable=E1103
1037
      try:
1038
        name = d["name"]
1039
      except KeyError:
1040
        base.ThrowError("'name' key missing from json object %s", devices)
1041

    
1042
      if name == volume_name:
1043
        if rbd_dev is not None:
1044
          base.ThrowError("rbd volume %s is mapped more than once", volume_name)
1045

    
1046
        rbd_dev = d["device"]
1047

    
1048
    return rbd_dev
1049

    
1050
  @staticmethod
1051
  def _ParseRbdShowmappedPlain(output, volume_name):
1052
    """Parse the (plain / text) output of `rbd showmapped'.
1053

1054
    This method parses the output of `rbd showmapped' and returns
1055
    the rbd block device path (e.g. /dev/rbd0) that matches the
1056
    given rbd volume.
1057

1058
    @type output: string
1059
    @param output: the plain text output of `rbd showmapped'
1060
    @type volume_name: string
1061
    @param volume_name: the name of the volume whose device we search for
1062
    @rtype: string or None
1063
    @return: block device path if the volume is mapped, else None
1064

1065
    """
1066
    allfields = 5
1067
    volumefield = 2
1068
    devicefield = 4
1069

    
1070
    lines = output.splitlines()
1071

    
1072
    # Try parsing the new output format (ceph >= 0.55).
1073
    splitted_lines = map(lambda l: l.split(), lines)
1074

    
1075
    # Check for empty output.
1076
    if not splitted_lines:
1077
      return None
1078

    
1079
    # Check showmapped output, to determine number of fields.
1080
    field_cnt = len(splitted_lines[0])
1081
    if field_cnt != allfields:
1082
      # Parsing the new format failed. Fallback to parsing the old output
1083
      # format (< 0.55).
1084
      splitted_lines = map(lambda l: l.split("\t"), lines)
1085
      if field_cnt != allfields:
1086
        base.ThrowError("Cannot parse rbd showmapped output expected %s fields,"
1087
                        " found %s", allfields, field_cnt)
1088

    
1089
    matched_lines = \
1090
      filter(lambda l: len(l) == allfields and l[volumefield] == volume_name,
1091
             splitted_lines)
1092

    
1093
    if len(matched_lines) > 1:
1094
      base.ThrowError("rbd volume %s mapped more than once", volume_name)
1095

    
1096
    if matched_lines:
1097
      # rbd block device found. Return it.
1098
      rbd_dev = matched_lines[0][devicefield]
1099
      return rbd_dev
1100

    
1101
    # The given volume is not mapped.
1102
    return None
1103

    
1104
  def Assemble(self):
1105
    """Assemble the device.
1106

1107
    """
1108
    pass
1109

    
1110
  def Shutdown(self):
1111
    """Shutdown the device.
1112

1113
    """
1114
    if not self.minor and not self.Attach():
1115
      # The rbd device doesn't exist.
1116
      return
1117

    
1118
    # Unmap the block device from the Volume.
1119
    self._UnmapVolumeFromBlockdev(self.unique_id)
1120

    
1121
    self.minor = None
1122
    self.dev_path = None
1123

    
1124
  def _UnmapVolumeFromBlockdev(self, unique_id):
1125
    """Unmaps the rbd device from the Volume it is mapped.
1126

1127
    Unmaps the rbd device from the Volume it was previously mapped to.
1128
    This method should be idempotent if the Volume isn't mapped.
1129

1130
    """
1131
    pool = self.params[constants.LDP_POOL]
1132
    name = unique_id[1]
1133

    
1134
    # Check if the mapping already exists.
1135
    rbd_dev = self._VolumeToBlockdev(pool, name)
1136

    
1137
    if rbd_dev:
1138
      # The mapping exists. Unmap the rbd device.
1139
      unmap_cmd = [constants.RBD_CMD, "unmap", "%s" % rbd_dev]
1140
      result = utils.RunCmd(unmap_cmd)
1141
      if result.failed:
1142
        base.ThrowError("rbd unmap failed (%s): %s",
1143
                        result.fail_reason, result.output)
1144

    
1145
  def Open(self, force=False):
1146
    """Make the device ready for I/O.
1147

1148
    """
1149
    pass
1150

    
1151
  def Close(self):
1152
    """Notifies that the device will no longer be used for I/O.
1153

1154
    """
1155
    pass
1156

    
1157
  def Grow(self, amount, dryrun, backingstore, excl_stor):
1158
    """Grow the Volume.
1159

1160
    @type amount: integer
1161
    @param amount: the amount (in mebibytes) to grow with
1162
    @type dryrun: boolean
1163
    @param dryrun: whether to execute the operation in simulation mode
1164
        only, without actually increasing the size
1165

1166
    """
1167
    if not backingstore:
1168
      return
1169
    if not self.Attach():
1170
      base.ThrowError("Can't attach to rbd device during Grow()")
1171

    
1172
    if dryrun:
1173
      # the rbd tool does not support dry runs of resize operations.
1174
      # Since rbd volumes are thinly provisioned, we assume
1175
      # there is always enough free space for the operation.
1176
      return
1177

    
1178
    rbd_pool = self.params[constants.LDP_POOL]
1179
    rbd_name = self.unique_id[1]
1180
    new_size = self.size + amount
1181

    
1182
    # Resize the rbd volume (Image) inside the RADOS cluster.
1183
    cmd = [constants.RBD_CMD, "resize", "-p", rbd_pool,
1184
           rbd_name, "--size", "%s" % new_size]
1185
    result = utils.RunCmd(cmd)
1186
    if result.failed:
1187
      base.ThrowError("rbd resize failed (%s): %s",
1188
                      result.fail_reason, result.output)
1189

    
1190
  def GetUserspaceAccessUri(self, hypervisor):
1191
    """Generate KVM userspace URIs to be used as `-drive file` settings.
1192

1193
    @see: L{BlockDev.GetUserspaceAccessUri}
1194

1195
    """
1196
    if hypervisor == constants.HT_KVM:
1197
      return "rbd:" + self.rbd_pool + "/" + self.rbd_name
1198
    else:
1199
      base.ThrowError("Hypervisor %s doesn't support RBD userspace access" %
1200
                      hypervisor)
1201

    
1202

    
1203
class ExtStorageDevice(base.BlockDev):
1204
  """A block device provided by an ExtStorage Provider.
1205

1206
  This class implements the External Storage Interface, which means
1207
  handling of the externally provided block devices.
1208

1209
  """
1210
  def __init__(self, unique_id, children, size, params, dyn_params, *args):
1211
    """Attaches to an extstorage block device.
1212

1213
    """
1214
    super(ExtStorageDevice, self).__init__(unique_id, children, size, params,
1215
                                           dyn_params, *args)
1216
    (self.name, self.uuid) = args
1217

    
1218
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1219
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1220

    
1221
    self.driver, self.vol_name = unique_id
1222
    self.ext_params = params
1223

    
1224
    self.major = self.minor = None
1225
    self.Attach()
1226

    
1227
  @classmethod
1228
  def Create(cls, unique_id, children, size, spindles, params, excl_stor,
1229
             dyn_params, *args):
1230
    """Create a new extstorage device.
1231

1232
    Provision a new volume using an extstorage provider, which will
1233
    then be mapped to a block device.
1234

1235
    """
1236
    (name, uuid) = args
1237

    
1238
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1239
      raise errors.ProgrammerError("Invalid configuration data %s" %
1240
                                   str(unique_id))
1241
    if excl_stor:
1242
      raise errors.ProgrammerError("extstorage device requested with"
1243
                                   " exclusive_storage")
1244

    
1245
    # Call the External Storage's create script,
1246
    # to provision a new Volume inside the External Storage
1247
    _ExtStorageAction(constants.ES_ACTION_CREATE, unique_id,
1248
                      params, size=str(size), name=name, uuid=uuid)
1249

    
1250
    return ExtStorageDevice(unique_id, children, size, params, dyn_params,
1251
                            *args)
1252

    
1253
  def Remove(self):
1254
    """Remove the extstorage device.
1255

1256
    """
1257
    if not self.minor and not self.Attach():
1258
      # The extstorage device doesn't exist.
1259
      return
1260

    
1261
    # First shutdown the device (remove mappings).
1262
    self.Shutdown()
1263

    
1264
    # Call the External Storage's remove script,
1265
    # to remove the Volume from the External Storage
1266
    _ExtStorageAction(constants.ES_ACTION_REMOVE, self.unique_id,
1267
                      self.ext_params, name=self.name, uuid=self.uuid)
1268

    
1269
  def Rename(self, new_id):
1270
    """Rename this device.
1271

1272
    """
1273
    pass
1274

    
1275
  def Attach(self):
1276
    """Attach to an existing extstorage device.
1277

1278
    This method maps the extstorage volume that matches our name with
1279
    a corresponding block device and then attaches to this device.
1280

1281
    """
1282
    self.attached = False
1283

    
1284
    # Call the External Storage's attach script,
1285
    # to attach an existing Volume to a block device under /dev
1286
    self.dev_path = _ExtStorageAction(constants.ES_ACTION_ATTACH,
1287
                                      self.unique_id, self.ext_params,
1288
                                      name=self.name, uuid=self.uuid)
1289

    
1290
    try:
1291
      st = os.stat(self.dev_path)
1292
    except OSError, err:
1293
      logging.error("Error stat()'ing %s: %s", self.dev_path, str(err))
1294
      return False
1295

    
1296
    if not stat.S_ISBLK(st.st_mode):
1297
      logging.error("%s is not a block device", self.dev_path)
1298
      return False
1299

    
1300
    self.major = os.major(st.st_rdev)
1301
    self.minor = os.minor(st.st_rdev)
1302
    self.attached = True
1303

    
1304
    return True
1305

    
1306
  def Assemble(self):
1307
    """Assemble the device.
1308

1309
    """
1310
    pass
1311

    
1312
  def Shutdown(self):
1313
    """Shutdown the device.
1314

1315
    """
1316
    if not self.minor and not self.Attach():
1317
      # The extstorage device doesn't exist.
1318
      return
1319

    
1320
    # Call the External Storage's detach script,
1321
    # to detach an existing Volume from it's block device under /dev
1322
    _ExtStorageAction(constants.ES_ACTION_DETACH, self.unique_id,
1323
                      self.ext_params, name=self.name, uuid=self.uuid)
1324

    
1325
    self.minor = None
1326
    self.dev_path = None
1327

    
1328
  def Open(self, force=False):
1329
    """Make the device ready for I/O.
1330

1331
    """
1332
    pass
1333

    
1334
  def Close(self):
1335
    """Notifies that the device will no longer be used for I/O.
1336

1337
    """
1338
    pass
1339

    
1340
  def Grow(self, amount, dryrun, backingstore, excl_stor):
1341
    """Grow the Volume.
1342

1343
    @type amount: integer
1344
    @param amount: the amount (in mebibytes) to grow with
1345
    @type dryrun: boolean
1346
    @param dryrun: whether to execute the operation in simulation mode
1347
        only, without actually increasing the size
1348

1349
    """
1350
    if not backingstore:
1351
      return
1352
    if not self.Attach():
1353
      base.ThrowError("Can't attach to extstorage device during Grow()")
1354

    
1355
    if dryrun:
1356
      # we do not support dry runs of resize operations for now.
1357
      return
1358

    
1359
    new_size = self.size + amount
1360

    
1361
    # Call the External Storage's grow script,
1362
    # to grow an existing Volume inside the External Storage
1363
    _ExtStorageAction(constants.ES_ACTION_GROW, self.unique_id,
1364
                      self.ext_params, size=str(self.size), grow=str(new_size),
1365
                      name=self.name, uuid=self.uuid)
1366

    
1367
  def SetInfo(self, text):
1368
    """Update metadata with info text.
1369

1370
    """
1371
    # Replace invalid characters
1372
    text = re.sub("^[^A-Za-z0-9_+.]", "_", text)
1373
    text = re.sub("[^-A-Za-z0-9_+.]", "_", text)
1374

    
1375
    # Only up to 128 characters are allowed
1376
    text = text[:128]
1377

    
1378
    # Call the External Storage's setinfo script,
1379
    # to set metadata for an existing Volume inside the External Storage
1380
    _ExtStorageAction(constants.ES_ACTION_SETINFO, self.unique_id,
1381
                      self.ext_params, metadata=text,
1382
                      name=self.name, uuid=self.uuid)
1383

    
1384

    
1385
def _ExtStorageAction(action, unique_id, ext_params,
1386
                      size=None, grow=None, metadata=None,
1387
                      name=None, uuid=None):
1388
  """Take an External Storage action.
1389

1390
  Take an External Storage action concerning or affecting
1391
  a specific Volume inside the External Storage.
1392

1393
  @type action: string
1394
  @param action: which action to perform. One of:
1395
                 create / remove / grow / attach / detach
1396
  @type unique_id: tuple (driver, vol_name)
1397
  @param unique_id: a tuple containing the type of ExtStorage (driver)
1398
                    and the Volume name
1399
  @type ext_params: dict
1400
  @param ext_params: ExtStorage parameters
1401
  @type size: integer
1402
  @param size: the size of the Volume in mebibytes
1403
  @type grow: integer
1404
  @param grow: the new size in mebibytes (after grow)
1405
  @type metadata: string
1406
  @param metadata: metadata info of the Volume, for use by the provider
1407
  @type name: string
1408
  @param name: name of the Volume (objects.Disk.name)
1409
  @type uuid: string
1410
  @param uuid: uuid of the Volume (objects.Disk.uuid)
1411
  @rtype: None or a block device path (during attach)
1412

1413
  """
1414
  driver, vol_name = unique_id
1415

    
1416
  # Create an External Storage instance of type `driver'
1417
  status, inst_es = ExtStorageFromDisk(driver)
1418
  if not status:
1419
    base.ThrowError("%s" % inst_es)
1420

    
1421
  # Create the basic environment for the driver's scripts
1422
  create_env = _ExtStorageEnvironment(unique_id, ext_params, size,
1423
                                      grow, metadata, name, uuid)
1424

    
1425
  # Do not use log file for action `attach' as we need
1426
  # to get the output from RunResult
1427
  # TODO: find a way to have a log file for attach too
1428
  logfile = None
1429
  if action is not constants.ES_ACTION_ATTACH:
1430
    logfile = _VolumeLogName(action, driver, vol_name)
1431

    
1432
  # Make sure the given action results in a valid script
1433
  if action not in constants.ES_SCRIPTS:
1434
    base.ThrowError("Action '%s' doesn't result in a valid ExtStorage script" %
1435
                    action)
1436

    
1437
  # Find out which external script to run according the given action
1438
  script_name = action + "_script"
1439
  script = getattr(inst_es, script_name)
1440

    
1441
  # Run the external script
1442
  result = utils.RunCmd([script], env=create_env,
1443
                        cwd=inst_es.path, output=logfile,)
1444
  if result.failed:
1445
    logging.error("External storage's %s command '%s' returned"
1446
                  " error: %s, logfile: %s, output: %s",
1447
                  action, result.cmd, result.fail_reason,
1448
                  logfile, result.output)
1449

    
1450
    # If logfile is 'None' (during attach), it breaks TailFile
1451
    # TODO: have a log file for attach too
1452
    if action is not constants.ES_ACTION_ATTACH:
1453
      lines = [utils.SafeEncode(val)
1454
               for val in utils.TailFile(logfile, lines=20)]
1455
    else:
1456
      lines = result.output[-20:]
1457

    
1458
    base.ThrowError("External storage's %s script failed (%s), last"
1459
                    " lines of output:\n%s",
1460
                    action, result.fail_reason, "\n".join(lines))
1461

    
1462
  if action == constants.ES_ACTION_ATTACH:
1463
    return result.stdout
1464

    
1465

    
1466
def ExtStorageFromDisk(name, base_dir=None):
1467
  """Create an ExtStorage instance from disk.
1468

1469
  This function will return an ExtStorage instance
1470
  if the given name is a valid ExtStorage name.
1471

1472
  @type base_dir: string
1473
  @keyword base_dir: Base directory containing ExtStorage installations.
1474
                     Defaults to a search in all the ES_SEARCH_PATH dirs.
1475
  @rtype: tuple
1476
  @return: True and the ExtStorage instance if we find a valid one, or
1477
      False and the diagnose message on error
1478

1479
  """
1480
  if base_dir is None:
1481
    es_base_dir = pathutils.ES_SEARCH_PATH
1482
  else:
1483
    es_base_dir = [base_dir]
1484

    
1485
  es_dir = utils.FindFile(name, es_base_dir, os.path.isdir)
1486

    
1487
  if es_dir is None:
1488
    return False, ("Directory for External Storage Provider %s not"
1489
                   " found in search path" % name)
1490

    
1491
  # ES Files dictionary, we will populate it with the absolute path
1492
  # names; if the value is True, then it is a required file, otherwise
1493
  # an optional one
1494
  es_files = dict.fromkeys(constants.ES_SCRIPTS, True)
1495

    
1496
  es_files[constants.ES_PARAMETERS_FILE] = True
1497

    
1498
  for (filename, _) in es_files.items():
1499
    es_files[filename] = utils.PathJoin(es_dir, filename)
1500

    
1501
    try:
1502
      st = os.stat(es_files[filename])
1503
    except EnvironmentError, err:
1504
      return False, ("File '%s' under path '%s' is missing (%s)" %
1505
                     (filename, es_dir, utils.ErrnoOrStr(err)))
1506

    
1507
    if not stat.S_ISREG(stat.S_IFMT(st.st_mode)):
1508
      return False, ("File '%s' under path '%s' is not a regular file" %
1509
                     (filename, es_dir))
1510

    
1511
    if filename in constants.ES_SCRIPTS:
1512
      if stat.S_IMODE(st.st_mode) & stat.S_IXUSR != stat.S_IXUSR:
1513
        return False, ("File '%s' under path '%s' is not executable" %
1514
                       (filename, es_dir))
1515

    
1516
  parameters = []
1517
  if constants.ES_PARAMETERS_FILE in es_files:
1518
    parameters_file = es_files[constants.ES_PARAMETERS_FILE]
1519
    try:
1520
      parameters = utils.ReadFile(parameters_file).splitlines()
1521
    except EnvironmentError, err:
1522
      return False, ("Error while reading the EXT parameters file at %s: %s" %
1523
                     (parameters_file, utils.ErrnoOrStr(err)))
1524
    parameters = [v.split(None, 1) for v in parameters]
1525

    
1526
  es_obj = \
1527
    objects.ExtStorage(name=name, path=es_dir,
1528
                       create_script=es_files[constants.ES_SCRIPT_CREATE],
1529
                       remove_script=es_files[constants.ES_SCRIPT_REMOVE],
1530
                       grow_script=es_files[constants.ES_SCRIPT_GROW],
1531
                       attach_script=es_files[constants.ES_SCRIPT_ATTACH],
1532
                       detach_script=es_files[constants.ES_SCRIPT_DETACH],
1533
                       setinfo_script=es_files[constants.ES_SCRIPT_SETINFO],
1534
                       verify_script=es_files[constants.ES_SCRIPT_VERIFY],
1535
                       supported_parameters=parameters)
1536
  return True, es_obj
1537

    
1538

    
1539
def _ExtStorageEnvironment(unique_id, ext_params,
1540
                           size=None, grow=None, metadata=None,
1541
                           name=None, uuid=None):
1542
  """Calculate the environment for an External Storage script.
1543

1544
  @type unique_id: tuple (driver, vol_name)
1545
  @param unique_id: ExtStorage pool and name of the Volume
1546
  @type ext_params: dict
1547
  @param ext_params: the EXT parameters
1548
  @type size: string
1549
  @param size: size of the Volume (in mebibytes)
1550
  @type grow: string
1551
  @param grow: new size of Volume after grow (in mebibytes)
1552
  @type metadata: string
1553
  @param metadata: metadata info of the Volume
1554
  @type name: string
1555
  @param name: name of the Volume (objects.Disk.name)
1556
  @type uuid: string
1557
  @param uuid: uuid of the Volume (objects.Disk.uuid)
1558
  @rtype: dict
1559
  @return: dict of environment variables
1560

1561
  """
1562
  vol_name = unique_id[1]
1563

    
1564
  result = {}
1565
  result["VOL_NAME"] = vol_name
1566

    
1567
  # EXT params
1568
  for pname, pvalue in ext_params.items():
1569
    result["EXTP_%s" % pname.upper()] = str(pvalue)
1570

    
1571
  if size is not None:
1572
    result["VOL_SIZE"] = size
1573

    
1574
  if grow is not None:
1575
    result["VOL_NEW_SIZE"] = grow
1576

    
1577
  if metadata is not None:
1578
    result["VOL_METADATA"] = metadata
1579

    
1580
  if name is not None:
1581
    result["VOL_CNAME"] = name
1582

    
1583
  if uuid is not None:
1584
    result["VOL_UUID"] = uuid
1585

    
1586
  return result
1587

    
1588

    
1589
def _VolumeLogName(kind, es_name, volume):
1590
  """Compute the ExtStorage log filename for a given Volume and operation.
1591

1592
  @type kind: string
1593
  @param kind: the operation type (e.g. create, remove etc.)
1594
  @type es_name: string
1595
  @param es_name: the ExtStorage name
1596
  @type volume: string
1597
  @param volume: the name of the Volume inside the External Storage
1598

1599
  """
1600
  # Check if the extstorage log dir is a valid dir
1601
  if not os.path.isdir(pathutils.LOG_ES_DIR):
1602
    base.ThrowError("Cannot find log directory: %s", pathutils.LOG_ES_DIR)
1603

    
1604
  # TODO: Use tempfile.mkstemp to create unique filename
1605
  basename = ("%s-%s-%s-%s.log" %
1606
              (kind, es_name, volume, utils.TimestampForFilename()))
1607
  return utils.PathJoin(pathutils.LOG_ES_DIR, basename)
1608

    
1609

    
1610
def _VerifyDiskType(dev_type):
1611
  if dev_type not in DEV_MAP:
1612
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1613

    
1614

    
1615
def _VerifyDiskParams(disk):
1616
  """Verifies if all disk parameters are set.
1617

1618
  """
1619
  missing = set(constants.DISK_LD_DEFAULTS[disk.dev_type]) - set(disk.params)
1620
  if missing:
1621
    raise errors.ProgrammerError("Block device is missing disk parameters: %s" %
1622
                                 missing)
1623

    
1624

    
1625
def FindDevice(disk, children):
1626
  """Search for an existing, assembled device.
1627

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

1631
  @type disk: L{objects.Disk}
1632
  @param disk: the disk object to find
1633
  @type children: list of L{bdev.BlockDev}
1634
  @param children: the list of block devices that are children of the device
1635
                  represented by the disk parameter
1636

1637
  """
1638
  _VerifyDiskType(disk.dev_type)
1639
  device = DEV_MAP[disk.dev_type](disk.logical_id, children, disk.size,
1640
                                  disk.params, disk.dynamic_params,
1641
                                  disk.name, disk.uuid)
1642
  if not device.attached:
1643
    return None
1644
  return device
1645

    
1646

    
1647
def Assemble(disk, children):
1648
  """Try to attach or assemble an existing device.
1649

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

1653
  @type disk: L{objects.Disk}
1654
  @param disk: the disk object to assemble
1655
  @type children: list of L{bdev.BlockDev}
1656
  @param children: the list of block devices that are children of the device
1657
                  represented by the disk parameter
1658

1659
  """
1660
  _VerifyDiskType(disk.dev_type)
1661
  _VerifyDiskParams(disk)
1662
  device = DEV_MAP[disk.dev_type](disk.logical_id, children, disk.size,
1663
                                  disk.params, disk.dynamic_params,
1664
                                  disk.name, disk.uuid)
1665
  device.Assemble()
1666
  return device
1667

    
1668

    
1669
def Create(disk, children, excl_stor):
1670
  """Create a device.
1671

1672
  @type disk: L{objects.Disk}
1673
  @param disk: the disk object to create
1674
  @type children: list of L{bdev.BlockDev}
1675
  @param children: the list of block devices that are children of the device
1676
                  represented by the disk parameter
1677
  @type excl_stor: boolean
1678
  @param excl_stor: Whether exclusive_storage is active
1679
  @rtype: L{bdev.BlockDev}
1680
  @return: the created device, or C{None} in case of an error
1681

1682
  """
1683
  _VerifyDiskType(disk.dev_type)
1684
  _VerifyDiskParams(disk)
1685
  device = DEV_MAP[disk.dev_type].Create(disk.logical_id, children, disk.size,
1686
                                         disk.spindles, disk.params, excl_stor,
1687
                                         disk.dynamic_params,
1688
                                         disk.name, disk.uuid)
1689
  return device
1690

    
1691
# Please keep this at the bottom of the file for visibility.
1692
DEV_MAP = {
1693
  constants.DT_PLAIN: LogicalVolume,
1694
  constants.DT_DRBD8: drbd.DRBD8Dev,
1695
  constants.DT_BLOCK: PersistentBlockDevice,
1696
  constants.DT_RBD: RADOSBlockDevice,
1697
  constants.DT_EXT: ExtStorageDevice,
1698
  constants.DT_FILE: FileStorage,
1699
  constants.DT_SHARED_FILE: FileStorage,
1700
  constants.DT_GLUSTER: GlusterStorage,
1701
}
1702
"""Map disk types to disk type classes.
1703

1704
@see: L{Assemble}, L{FindDevice}, L{Create}.""" # pylint: disable=W0105