Statistics
| Branch: | Tag: | Revision:

root / lib / storage / bdev.py @ 2656b017

History | View | Annotate | Download (53.4 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

    
43

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

47
  """
48
  pass
49

    
50

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

54
  @param result: result from RunCmd
55

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

    
61

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
242
      data.append(splitted_fields)
243

    
244
    return data
245

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

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

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

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

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

    
301
    return data
302

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

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

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

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

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

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

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

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

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

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

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

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

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

    
383
    return data
384

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

540
    """
541
    pass
542

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

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

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

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

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

562
    @rtype: objects.BlockDevStatus
563

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

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

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

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

583
    """
584
    pass
585

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

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

591
    """
592
    pass
593

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

597
    @returns: tuple (vg, lv)
598

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

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

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

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

    
618
    return (self._vg_name, snap_name)
619

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

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

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

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

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

    
640
    self._RemoveOldInfo()
641

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

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

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

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

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

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

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

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

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

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

    
715

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

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

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

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

729
    The unique_id is a path under /dev.
730

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

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

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

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

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

    
764
  def Remove(self):
765
    """Remove a device
766

767
    This is a noop
768

769
    """
770
    pass
771

    
772
  def Rename(self, new_id):
773
    """Rename this device.
774

775
    """
776
    base.ThrowError("Rename is not supported for PersistentBlockDev storage")
777

    
778
  def Attach(self):
779
    """Attach to an existing block device.
780

781

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

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

    
794
    self.major = os.major(st.st_rdev)
795
    self.minor = os.minor(st.st_rdev)
796
    self.attached = True
797

    
798
    return True
799

    
800
  def Assemble(self):
801
    """Assemble the device.
802

803
    """
804
    pass
805

    
806
  def Shutdown(self):
807
    """Shutdown the device.
808

809
    """
810
    pass
811

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

815
    """
816
    pass
817

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

821
    """
822
    pass
823

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

827
    """
828
    base.ThrowError("Grow is not supported for PersistentBlockDev storage")
829

    
830

    
831
class RADOSBlockDevice(base.BlockDev):
832
  """A RADOS Block Device (rbd).
833

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

838
  """
839
  def __init__(self, unique_id, children, size, params, dyn_params):
840
    """Attaches to an rbd device.
841

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

    
848
    self.driver, self.rbd_name = unique_id
849
    self.rbd_pool = params[constants.LDP_POOL]
850

    
851
    self.major = self.minor = None
852
    self.Attach()
853

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

859
    Provision a new rbd volume inside a RADOS pool.
860

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

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

    
879
    return RADOSBlockDevice(unique_id, children, size, params, dyn_params)
880

    
881
  def Remove(self):
882
    """Remove the rbd device.
883

884
    """
885
    rbd_pool = self.params[constants.LDP_POOL]
886
    rbd_name = self.unique_id[1]
887

    
888
    if not self.minor and not self.Attach():
889
      # The rbd device doesn't exist.
890
      return
891

    
892
    # First shutdown the device (remove mappings).
893
    self.Shutdown()
894

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

    
902
  def Rename(self, new_id):
903
    """Rename this device.
904

905
    """
906
    pass
907

    
908
  def Attach(self):
909
    """Attach to an existing rbd device.
910

911
    This method maps the rbd volume that matches our name with
912
    an rbd device and then attaches to this device.
913

914
    """
915
    self.attached = False
916

    
917
    # Map the rbd volume to a block device under /dev
918
    self.dev_path = self._MapVolumeToBlockdev(self.unique_id)
919

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

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

    
930
    self.major = os.major(st.st_rdev)
931
    self.minor = os.minor(st.st_rdev)
932
    self.attached = True
933

    
934
    return True
935

    
936
  def _MapVolumeToBlockdev(self, unique_id):
937
    """Maps existing rbd volumes to block devices.
938

939
    This method should be idempotent if the mapping already exists.
940

941
    @rtype: string
942
    @return: the block device path that corresponds to the volume
943

944
    """
945
    pool = self.params[constants.LDP_POOL]
946
    name = unique_id[1]
947

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

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

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

    
967
    # The device was successfully mapped. Return it.
968
    return rbd_dev
969

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

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

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

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

    
1010
      return cls._ParseRbdShowmappedPlain(result.output, volume_name)
1011

    
1012
  @staticmethod
1013
  def _ParseRbdShowmappedJson(output, volume_name):
1014
    """Parse the json output of `rbd showmapped'.
1015

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

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

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

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

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

    
1043
        rbd_dev = d["device"]
1044

    
1045
    return rbd_dev
1046

    
1047
  @staticmethod
1048
  def _ParseRbdShowmappedPlain(output, volume_name):
1049
    """Parse the (plain / text) output of `rbd showmapped'.
1050

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

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

1062
    """
1063
    allfields = 5
1064
    volumefield = 2
1065
    devicefield = 4
1066

    
1067
    lines = output.splitlines()
1068

    
1069
    # Try parsing the new output format (ceph >= 0.55).
1070
    splitted_lines = map(lambda l: l.split(), lines)
1071

    
1072
    # Check for empty output.
1073
    if not splitted_lines:
1074
      return None
1075

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

    
1086
    matched_lines = \
1087
      filter(lambda l: len(l) == allfields and l[volumefield] == volume_name,
1088
             splitted_lines)
1089

    
1090
    if len(matched_lines) > 1:
1091
      base.ThrowError("rbd volume %s mapped more than once", volume_name)
1092

    
1093
    if matched_lines:
1094
      # rbd block device found. Return it.
1095
      rbd_dev = matched_lines[0][devicefield]
1096
      return rbd_dev
1097

    
1098
    # The given volume is not mapped.
1099
    return None
1100

    
1101
  def Assemble(self):
1102
    """Assemble the device.
1103

1104
    """
1105
    pass
1106

    
1107
  def Shutdown(self):
1108
    """Shutdown the device.
1109

1110
    """
1111
    if not self.minor and not self.Attach():
1112
      # The rbd device doesn't exist.
1113
      return
1114

    
1115
    # Unmap the block device from the Volume.
1116
    self._UnmapVolumeFromBlockdev(self.unique_id)
1117

    
1118
    self.minor = None
1119
    self.dev_path = None
1120

    
1121
  def _UnmapVolumeFromBlockdev(self, unique_id):
1122
    """Unmaps the rbd device from the Volume it is mapped.
1123

1124
    Unmaps the rbd device from the Volume it was previously mapped to.
1125
    This method should be idempotent if the Volume isn't mapped.
1126

1127
    """
1128
    pool = self.params[constants.LDP_POOL]
1129
    name = unique_id[1]
1130

    
1131
    # Check if the mapping already exists.
1132
    rbd_dev = self._VolumeToBlockdev(pool, name)
1133

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

    
1142
  def Open(self, force=False):
1143
    """Make the device ready for I/O.
1144

1145
    """
1146
    pass
1147

    
1148
  def Close(self):
1149
    """Notifies that the device will no longer be used for I/O.
1150

1151
    """
1152
    pass
1153

    
1154
  def Grow(self, amount, dryrun, backingstore, excl_stor):
1155
    """Grow the Volume.
1156

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

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

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

    
1175
    rbd_pool = self.params[constants.LDP_POOL]
1176
    rbd_name = self.unique_id[1]
1177
    new_size = self.size + amount
1178

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

    
1187
  def GetUserspaceAccessUri(self, hypervisor):
1188
    """Generate KVM userspace URIs to be used as `-drive file` settings.
1189

1190
    @see: L{BlockDev.GetUserspaceAccessUri}
1191

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

    
1199

    
1200
class ExtStorageDevice(base.BlockDev):
1201
  """A block device provided by an ExtStorage Provider.
1202

1203
  This class implements the External Storage Interface, which means
1204
  handling of the externally provided block devices.
1205

1206
  """
1207
  def __init__(self, unique_id, children, size, params, dyn_params):
1208
    """Attaches to an extstorage block device.
1209

1210
    """
1211
    super(ExtStorageDevice, self).__init__(unique_id, children, size, params,
1212
                                           dyn_params)
1213
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1214
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1215

    
1216
    self.driver, self.vol_name = unique_id
1217
    self.ext_params = params
1218

    
1219
    self.major = self.minor = None
1220
    self.Attach()
1221

    
1222
  @classmethod
1223
  def Create(cls, unique_id, children, size, spindles, params, excl_stor,
1224
             dyn_params):
1225
    """Create a new extstorage device.
1226

1227
    Provision a new volume using an extstorage provider, which will
1228
    then be mapped to a block device.
1229

1230
    """
1231
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1232
      raise errors.ProgrammerError("Invalid configuration data %s" %
1233
                                   str(unique_id))
1234
    if excl_stor:
1235
      raise errors.ProgrammerError("extstorage device requested with"
1236
                                   " exclusive_storage")
1237

    
1238
    # Call the External Storage's create script,
1239
    # to provision a new Volume inside the External Storage
1240
    _ExtStorageAction(constants.ES_ACTION_CREATE, unique_id,
1241
                      params, str(size))
1242

    
1243
    return ExtStorageDevice(unique_id, children, size, params, dyn_params)
1244

    
1245
  def Remove(self):
1246
    """Remove the extstorage device.
1247

1248
    """
1249
    if not self.minor and not self.Attach():
1250
      # The extstorage device doesn't exist.
1251
      return
1252

    
1253
    # First shutdown the device (remove mappings).
1254
    self.Shutdown()
1255

    
1256
    # Call the External Storage's remove script,
1257
    # to remove the Volume from the External Storage
1258
    _ExtStorageAction(constants.ES_ACTION_REMOVE, self.unique_id,
1259
                      self.ext_params)
1260

    
1261
  def Rename(self, new_id):
1262
    """Rename this device.
1263

1264
    """
1265
    pass
1266

    
1267
  def Attach(self):
1268
    """Attach to an existing extstorage device.
1269

1270
    This method maps the extstorage volume that matches our name with
1271
    a corresponding block device and then attaches to this device.
1272

1273
    """
1274
    self.attached = False
1275

    
1276
    # Call the External Storage's attach script,
1277
    # to attach an existing Volume to a block device under /dev
1278
    self.dev_path = _ExtStorageAction(constants.ES_ACTION_ATTACH,
1279
                                      self.unique_id, self.ext_params)
1280

    
1281
    try:
1282
      st = os.stat(self.dev_path)
1283
    except OSError, err:
1284
      logging.error("Error stat()'ing %s: %s", self.dev_path, str(err))
1285
      return False
1286

    
1287
    if not stat.S_ISBLK(st.st_mode):
1288
      logging.error("%s is not a block device", self.dev_path)
1289
      return False
1290

    
1291
    self.major = os.major(st.st_rdev)
1292
    self.minor = os.minor(st.st_rdev)
1293
    self.attached = True
1294

    
1295
    return True
1296

    
1297
  def Assemble(self):
1298
    """Assemble the device.
1299

1300
    """
1301
    pass
1302

    
1303
  def Shutdown(self):
1304
    """Shutdown the device.
1305

1306
    """
1307
    if not self.minor and not self.Attach():
1308
      # The extstorage device doesn't exist.
1309
      return
1310

    
1311
    # Call the External Storage's detach script,
1312
    # to detach an existing Volume from it's block device under /dev
1313
    _ExtStorageAction(constants.ES_ACTION_DETACH, self.unique_id,
1314
                      self.ext_params)
1315

    
1316
    self.minor = None
1317
    self.dev_path = None
1318

    
1319
  def Open(self, force=False):
1320
    """Make the device ready for I/O.
1321

1322
    """
1323
    pass
1324

    
1325
  def Close(self):
1326
    """Notifies that the device will no longer be used for I/O.
1327

1328
    """
1329
    pass
1330

    
1331
  def Grow(self, amount, dryrun, backingstore, excl_stor):
1332
    """Grow the Volume.
1333

1334
    @type amount: integer
1335
    @param amount: the amount (in mebibytes) to grow with
1336
    @type dryrun: boolean
1337
    @param dryrun: whether to execute the operation in simulation mode
1338
        only, without actually increasing the size
1339

1340
    """
1341
    if not backingstore:
1342
      return
1343
    if not self.Attach():
1344
      base.ThrowError("Can't attach to extstorage device during Grow()")
1345

    
1346
    if dryrun:
1347
      # we do not support dry runs of resize operations for now.
1348
      return
1349

    
1350
    new_size = self.size + amount
1351

    
1352
    # Call the External Storage's grow script,
1353
    # to grow an existing Volume inside the External Storage
1354
    _ExtStorageAction(constants.ES_ACTION_GROW, self.unique_id,
1355
                      self.ext_params, str(self.size), grow=str(new_size))
1356

    
1357
  def SetInfo(self, text):
1358
    """Update metadata with info text.
1359

1360
    """
1361
    # Replace invalid characters
1362
    text = re.sub("^[^A-Za-z0-9_+.]", "_", text)
1363
    text = re.sub("[^-A-Za-z0-9_+.]", "_", text)
1364

    
1365
    # Only up to 128 characters are allowed
1366
    text = text[:128]
1367

    
1368
    # Call the External Storage's setinfo script,
1369
    # to set metadata for an existing Volume inside the External Storage
1370
    _ExtStorageAction(constants.ES_ACTION_SETINFO, self.unique_id,
1371
                      self.ext_params, metadata=text)
1372

    
1373

    
1374
def _ExtStorageAction(action, unique_id, ext_params,
1375
                      size=None, grow=None, metadata=None):
1376
  """Take an External Storage action.
1377

1378
  Take an External Storage action concerning or affecting
1379
  a specific Volume inside the External Storage.
1380

1381
  @type action: string
1382
  @param action: which action to perform. One of:
1383
                 create / remove / grow / attach / detach
1384
  @type unique_id: tuple (driver, vol_name)
1385
  @param unique_id: a tuple containing the type of ExtStorage (driver)
1386
                    and the Volume name
1387
  @type ext_params: dict
1388
  @param ext_params: ExtStorage parameters
1389
  @type size: integer
1390
  @param size: the size of the Volume in mebibytes
1391
  @type grow: integer
1392
  @param grow: the new size in mebibytes (after grow)
1393
  @type metadata: string
1394
  @param metadata: metadata info of the Volume, for use by the provider
1395
  @rtype: None or a block device path (during attach)
1396

1397
  """
1398
  driver, vol_name = unique_id
1399

    
1400
  # Create an External Storage instance of type `driver'
1401
  status, inst_es = ExtStorageFromDisk(driver)
1402
  if not status:
1403
    base.ThrowError("%s" % inst_es)
1404

    
1405
  # Create the basic environment for the driver's scripts
1406
  create_env = _ExtStorageEnvironment(unique_id, ext_params, size,
1407
                                      grow, metadata)
1408

    
1409
  # Do not use log file for action `attach' as we need
1410
  # to get the output from RunResult
1411
  # TODO: find a way to have a log file for attach too
1412
  logfile = None
1413
  if action is not constants.ES_ACTION_ATTACH:
1414
    logfile = _VolumeLogName(action, driver, vol_name)
1415

    
1416
  # Make sure the given action results in a valid script
1417
  if action not in constants.ES_SCRIPTS:
1418
    base.ThrowError("Action '%s' doesn't result in a valid ExtStorage script" %
1419
                    action)
1420

    
1421
  # Find out which external script to run according the given action
1422
  script_name = action + "_script"
1423
  script = getattr(inst_es, script_name)
1424

    
1425
  # Run the external script
1426
  result = utils.RunCmd([script], env=create_env,
1427
                        cwd=inst_es.path, output=logfile,)
1428
  if result.failed:
1429
    logging.error("External storage's %s command '%s' returned"
1430
                  " error: %s, logfile: %s, output: %s",
1431
                  action, result.cmd, result.fail_reason,
1432
                  logfile, result.output)
1433

    
1434
    # If logfile is 'None' (during attach), it breaks TailFile
1435
    # TODO: have a log file for attach too
1436
    if action is not constants.ES_ACTION_ATTACH:
1437
      lines = [utils.SafeEncode(val)
1438
               for val in utils.TailFile(logfile, lines=20)]
1439
    else:
1440
      lines = result.output[-20:]
1441

    
1442
    base.ThrowError("External storage's %s script failed (%s), last"
1443
                    " lines of output:\n%s",
1444
                    action, result.fail_reason, "\n".join(lines))
1445

    
1446
  if action == constants.ES_ACTION_ATTACH:
1447
    return result.stdout
1448

    
1449

    
1450
def ExtStorageFromDisk(name, base_dir=None):
1451
  """Create an ExtStorage instance from disk.
1452

1453
  This function will return an ExtStorage instance
1454
  if the given name is a valid ExtStorage name.
1455

1456
  @type base_dir: string
1457
  @keyword base_dir: Base directory containing ExtStorage installations.
1458
                     Defaults to a search in all the ES_SEARCH_PATH dirs.
1459
  @rtype: tuple
1460
  @return: True and the ExtStorage instance if we find a valid one, or
1461
      False and the diagnose message on error
1462

1463
  """
1464
  if base_dir is None:
1465
    es_base_dir = pathutils.ES_SEARCH_PATH
1466
  else:
1467
    es_base_dir = [base_dir]
1468

    
1469
  es_dir = utils.FindFile(name, es_base_dir, os.path.isdir)
1470

    
1471
  if es_dir is None:
1472
    return False, ("Directory for External Storage Provider %s not"
1473
                   " found in search path" % name)
1474

    
1475
  # ES Files dictionary, we will populate it with the absolute path
1476
  # names; if the value is True, then it is a required file, otherwise
1477
  # an optional one
1478
  es_files = dict.fromkeys(constants.ES_SCRIPTS, True)
1479

    
1480
  es_files[constants.ES_PARAMETERS_FILE] = True
1481

    
1482
  for (filename, _) in es_files.items():
1483
    es_files[filename] = utils.PathJoin(es_dir, filename)
1484

    
1485
    try:
1486
      st = os.stat(es_files[filename])
1487
    except EnvironmentError, err:
1488
      return False, ("File '%s' under path '%s' is missing (%s)" %
1489
                     (filename, es_dir, utils.ErrnoOrStr(err)))
1490

    
1491
    if not stat.S_ISREG(stat.S_IFMT(st.st_mode)):
1492
      return False, ("File '%s' under path '%s' is not a regular file" %
1493
                     (filename, es_dir))
1494

    
1495
    if filename in constants.ES_SCRIPTS:
1496
      if stat.S_IMODE(st.st_mode) & stat.S_IXUSR != stat.S_IXUSR:
1497
        return False, ("File '%s' under path '%s' is not executable" %
1498
                       (filename, es_dir))
1499

    
1500
  parameters = []
1501
  if constants.ES_PARAMETERS_FILE in es_files:
1502
    parameters_file = es_files[constants.ES_PARAMETERS_FILE]
1503
    try:
1504
      parameters = utils.ReadFile(parameters_file).splitlines()
1505
    except EnvironmentError, err:
1506
      return False, ("Error while reading the EXT parameters file at %s: %s" %
1507
                     (parameters_file, utils.ErrnoOrStr(err)))
1508
    parameters = [v.split(None, 1) for v in parameters]
1509

    
1510
  es_obj = \
1511
    objects.ExtStorage(name=name, path=es_dir,
1512
                       create_script=es_files[constants.ES_SCRIPT_CREATE],
1513
                       remove_script=es_files[constants.ES_SCRIPT_REMOVE],
1514
                       grow_script=es_files[constants.ES_SCRIPT_GROW],
1515
                       attach_script=es_files[constants.ES_SCRIPT_ATTACH],
1516
                       detach_script=es_files[constants.ES_SCRIPT_DETACH],
1517
                       setinfo_script=es_files[constants.ES_SCRIPT_SETINFO],
1518
                       verify_script=es_files[constants.ES_SCRIPT_VERIFY],
1519
                       supported_parameters=parameters)
1520
  return True, es_obj
1521

    
1522

    
1523
def _ExtStorageEnvironment(unique_id, ext_params,
1524
                           size=None, grow=None, metadata=None):
1525
  """Calculate the environment for an External Storage script.
1526

1527
  @type unique_id: tuple (driver, vol_name)
1528
  @param unique_id: ExtStorage pool and name of the Volume
1529
  @type ext_params: dict
1530
  @param ext_params: the EXT parameters
1531
  @type size: string
1532
  @param size: size of the Volume (in mebibytes)
1533
  @type grow: string
1534
  @param grow: new size of Volume after grow (in mebibytes)
1535
  @type metadata: string
1536
  @param metadata: metadata info of the Volume
1537
  @rtype: dict
1538
  @return: dict of environment variables
1539

1540
  """
1541
  vol_name = unique_id[1]
1542

    
1543
  result = {}
1544
  result["VOL_NAME"] = vol_name
1545

    
1546
  # EXT params
1547
  for pname, pvalue in ext_params.items():
1548
    result["EXTP_%s" % pname.upper()] = str(pvalue)
1549

    
1550
  if size is not None:
1551
    result["VOL_SIZE"] = size
1552

    
1553
  if grow is not None:
1554
    result["VOL_NEW_SIZE"] = grow
1555

    
1556
  if metadata is not None:
1557
    result["VOL_METADATA"] = metadata
1558

    
1559
  return result
1560

    
1561

    
1562
def _VolumeLogName(kind, es_name, volume):
1563
  """Compute the ExtStorage log filename for a given Volume and operation.
1564

1565
  @type kind: string
1566
  @param kind: the operation type (e.g. create, remove etc.)
1567
  @type es_name: string
1568
  @param es_name: the ExtStorage name
1569
  @type volume: string
1570
  @param volume: the name of the Volume inside the External Storage
1571

1572
  """
1573
  # Check if the extstorage log dir is a valid dir
1574
  if not os.path.isdir(pathutils.LOG_ES_DIR):
1575
    base.ThrowError("Cannot find log directory: %s", pathutils.LOG_ES_DIR)
1576

    
1577
  # TODO: Use tempfile.mkstemp to create unique filename
1578
  basename = ("%s-%s-%s-%s.log" %
1579
              (kind, es_name, volume, utils.TimestampForFilename()))
1580
  return utils.PathJoin(pathutils.LOG_ES_DIR, basename)
1581

    
1582

    
1583
def _VerifyDiskType(dev_type):
1584
  if dev_type not in DEV_MAP:
1585
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1586

    
1587

    
1588
def _VerifyDiskParams(disk):
1589
  """Verifies if all disk parameters are set.
1590

1591
  """
1592
  missing = set(constants.DISK_LD_DEFAULTS[disk.dev_type]) - set(disk.params)
1593
  if missing:
1594
    raise errors.ProgrammerError("Block device is missing disk parameters: %s" %
1595
                                 missing)
1596

    
1597

    
1598
def FindDevice(disk, children):
1599
  """Search for an existing, assembled device.
1600

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

1604
  @type disk: L{objects.Disk}
1605
  @param disk: the disk object to find
1606
  @type children: list of L{bdev.BlockDev}
1607
  @param children: the list of block devices that are children of the device
1608
                  represented by the disk parameter
1609

1610
  """
1611
  _VerifyDiskType(disk.dev_type)
1612
  device = DEV_MAP[disk.dev_type](disk.logical_id, children, disk.size,
1613
                                  disk.params, disk.dynamic_params)
1614
  if not device.attached:
1615
    return None
1616
  return device
1617

    
1618

    
1619
def Assemble(disk, children):
1620
  """Try to attach or assemble an existing device.
1621

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

1625
  @type disk: L{objects.Disk}
1626
  @param disk: the disk object to assemble
1627
  @type children: list of L{bdev.BlockDev}
1628
  @param children: the list of block devices that are children of the device
1629
                  represented by the disk parameter
1630

1631
  """
1632
  _VerifyDiskType(disk.dev_type)
1633
  _VerifyDiskParams(disk)
1634
  device = DEV_MAP[disk.dev_type](disk.logical_id, children, disk.size,
1635
                                  disk.params, disk.dynamic_params)
1636
  device.Assemble()
1637
  return device
1638

    
1639

    
1640
def Create(disk, children, excl_stor):
1641
  """Create a device.
1642

1643
  @type disk: L{objects.Disk}
1644
  @param disk: the disk object to create
1645
  @type children: list of L{bdev.BlockDev}
1646
  @param children: the list of block devices that are children of the device
1647
                  represented by the disk parameter
1648
  @type excl_stor: boolean
1649
  @param excl_stor: Whether exclusive_storage is active
1650
  @rtype: L{bdev.BlockDev}
1651
  @return: the created device, or C{None} in case of an error
1652

1653
  """
1654
  _VerifyDiskType(disk.dev_type)
1655
  _VerifyDiskParams(disk)
1656
  device = DEV_MAP[disk.dev_type].Create(disk.logical_id, children, disk.size,
1657
                                         disk.spindles, disk.params, excl_stor,
1658
                                         disk.dynamic_params)
1659
  return device
1660

    
1661
# Please keep this at the bottom of the file for visibility.
1662
DEV_MAP = {
1663
  constants.DT_PLAIN: LogicalVolume,
1664
  constants.DT_DRBD8: drbd.DRBD8Dev,
1665
  constants.DT_BLOCK: PersistentBlockDevice,
1666
  constants.DT_RBD: RADOSBlockDevice,
1667
  constants.DT_EXT: ExtStorageDevice,
1668
  constants.DT_FILE: FileStorage,
1669
  constants.DT_SHARED_FILE: FileStorage,
1670
}
1671
"""Map disk types to disk type classes.
1672

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