Statistics
| Branch: | Tag: | Revision:

root / lib / storage / bdev.py @ b5d48e87

History | View | Annotate | Download (56.9 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
import re
25
import errno
26
import stat
27
import os
28
import logging
29
import math
30

    
31
from ganeti import utils
32
from ganeti import errors
33
from ganeti import constants
34
from ganeti import objects
35
from ganeti import compat
36
from ganeti import pathutils
37
from ganeti import serializer
38
from ganeti.storage import drbd
39
from ganeti.storage import base
40

    
41

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

45
  """
46
  pass
47

    
48

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

52
  @param result: result from RunCmd
53

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

    
59

    
60
def _GetForbiddenFileStoragePaths():
61
  """Builds a list of path prefixes which shouldn't be used for file storage.
62

63
  @rtype: frozenset
64

65
  """
66
  paths = set([
67
    "/boot",
68
    "/dev",
69
    "/etc",
70
    "/home",
71
    "/proc",
72
    "/root",
73
    "/sys",
74
    ])
75

    
76
  for prefix in ["", "/usr", "/usr/local"]:
77
    paths.update(map(lambda s: "%s/%s" % (prefix, s),
78
                     ["bin", "lib", "lib32", "lib64", "sbin"]))
79

    
80
  return compat.UniqueFrozenset(map(os.path.normpath, paths))
81

    
82

    
83
def _ComputeWrongFileStoragePaths(paths,
84
                                  _forbidden=_GetForbiddenFileStoragePaths()):
85
  """Cross-checks a list of paths for prefixes considered bad.
86

87
  Some paths, e.g. "/bin", should not be used for file storage.
88

89
  @type paths: list
90
  @param paths: List of paths to be checked
91
  @rtype: list
92
  @return: Sorted list of paths for which the user should be warned
93

94
  """
95
  def _Check(path):
96
    return (not os.path.isabs(path) or
97
            path in _forbidden or
98
            filter(lambda p: utils.IsBelowDir(p, path), _forbidden))
99

    
100
  return utils.NiceSort(filter(_Check, map(os.path.normpath, paths)))
101

    
102

    
103
def ComputeWrongFileStoragePaths(_filename=pathutils.FILE_STORAGE_PATHS_FILE):
104
  """Returns a list of file storage paths whose prefix is considered bad.
105

106
  See L{_ComputeWrongFileStoragePaths}.
107

108
  """
109
  return _ComputeWrongFileStoragePaths(_LoadAllowedFileStoragePaths(_filename))
110

    
111

    
112
def _CheckFileStoragePath(path, allowed):
113
  """Checks if a path is in a list of allowed paths for file storage.
114

115
  @type path: string
116
  @param path: Path to check
117
  @type allowed: list
118
  @param allowed: List of allowed paths
119
  @raise errors.FileStoragePathError: If the path is not allowed
120

121
  """
122
  if not os.path.isabs(path):
123
    raise errors.FileStoragePathError("File storage path must be absolute,"
124
                                      " got '%s'" % path)
125

    
126
  for i in allowed:
127
    if not os.path.isabs(i):
128
      logging.info("Ignoring relative path '%s' for file storage", i)
129
      continue
130

    
131
    if utils.IsBelowDir(i, path):
132
      break
133
  else:
134
    raise errors.FileStoragePathError("Path '%s' is not acceptable for file"
135
                                      " storage" % path)
136

    
137

    
138
def _LoadAllowedFileStoragePaths(filename):
139
  """Loads file containing allowed file storage paths.
140

141
  @rtype: list
142
  @return: List of allowed paths (can be an empty list)
143

144
  """
145
  try:
146
    contents = utils.ReadFile(filename)
147
  except EnvironmentError:
148
    return []
149
  else:
150
    return utils.FilterEmptyLinesAndComments(contents)
151

    
152

    
153
def CheckFileStoragePath(path, _filename=pathutils.FILE_STORAGE_PATHS_FILE):
154
  """Checks if a path is allowed for file storage.
155

156
  @type path: string
157
  @param path: Path to check
158
  @raise errors.FileStoragePathError: If the path is not allowed
159

160
  """
161
  allowed = _LoadAllowedFileStoragePaths(_filename)
162

    
163
  if _ComputeWrongFileStoragePaths([path]):
164
    raise errors.FileStoragePathError("Path '%s' uses a forbidden prefix" %
165
                                      path)
166

    
167
  _CheckFileStoragePath(path, allowed)
168

    
169

    
170
class LogicalVolume(base.BlockDev):
171
  """Logical Volume block device.
172

173
  """
174
  _VALID_NAME_RE = re.compile("^[a-zA-Z0-9+_.-]*$")
175
  _PARSE_PV_DEV_RE = re.compile("^([^ ()]+)\([0-9]+\)$")
176
  _INVALID_NAMES = compat.UniqueFrozenset([".", "..", "snapshot", "pvmove"])
177
  _INVALID_SUBSTRINGS = compat.UniqueFrozenset(["_mlog", "_mimage"])
178

    
179
  def __init__(self, unique_id, children, size, params):
180
    """Attaches to a LV device.
181

182
    The unique_id is a tuple (vg_name, lv_name)
183

184
    """
185
    super(LogicalVolume, self).__init__(unique_id, children, size, params)
186
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
187
      raise ValueError("Invalid configuration data %s" % str(unique_id))
188
    self._vg_name, self._lv_name = unique_id
189
    self._ValidateName(self._vg_name)
190
    self._ValidateName(self._lv_name)
191
    self.dev_path = utils.PathJoin("/dev", self._vg_name, self._lv_name)
192
    self._degraded = True
193
    self.major = self.minor = self.pe_size = self.stripe_count = None
194
    self.pv_names = None
195
    self.Attach()
196

    
197
  @staticmethod
198
  def _GetStdPvSize(pvs_info):
199
    """Return the the standard PV size (used with exclusive storage).
200

201
    @param pvs_info: list of objects.LvmPvInfo, cannot be empty
202
    @rtype: float
203
    @return: size in MiB
204

205
    """
206
    assert len(pvs_info) > 0
207
    smallest = min([pv.size for pv in pvs_info])
208
    return smallest / (1 + constants.PART_MARGIN + constants.PART_RESERVED)
209

    
210
  @staticmethod
211
  def _ComputeNumPvs(size, pvs_info):
212
    """Compute the number of PVs needed for an LV (with exclusive storage).
213

214
    @type size: float
215
    @param size: LV size in MiB
216
    @param pvs_info: list of objects.LvmPvInfo, cannot be empty
217
    @rtype: integer
218
    @return: number of PVs needed
219
    """
220
    assert len(pvs_info) > 0
221
    pv_size = float(LogicalVolume._GetStdPvSize(pvs_info))
222
    return int(math.ceil(float(size) / pv_size))
223

    
224
  @staticmethod
225
  def _GetEmptyPvNames(pvs_info, max_pvs=None):
226
    """Return a list of empty PVs, by name.
227

228
    """
229
    empty_pvs = filter(objects.LvmPvInfo.IsEmpty, pvs_info)
230
    if max_pvs is not None:
231
      empty_pvs = empty_pvs[:max_pvs]
232
    return map((lambda pv: pv.name), empty_pvs)
233

    
234
  @classmethod
235
  def Create(cls, unique_id, children, size, spindles, params, excl_stor):
236
    """Create a new logical volume.
237

238
    """
239
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
240
      raise errors.ProgrammerError("Invalid configuration data %s" %
241
                                   str(unique_id))
242
    vg_name, lv_name = unique_id
243
    cls._ValidateName(vg_name)
244
    cls._ValidateName(lv_name)
245
    pvs_info = cls.GetPVInfo([vg_name])
246
    if not pvs_info:
247
      if excl_stor:
248
        msg = "No (empty) PVs found"
249
      else:
250
        msg = "Can't compute PV info for vg %s" % vg_name
251
      base.ThrowError(msg)
252
    pvs_info.sort(key=(lambda pv: pv.free), reverse=True)
253

    
254
    pvlist = [pv.name for pv in pvs_info]
255
    if compat.any(":" in v for v in pvlist):
256
      base.ThrowError("Some of your PVs have the invalid character ':' in their"
257
                      " name, this is not supported - please filter them out"
258
                      " in lvm.conf using either 'filter' or 'preferred_names'")
259

    
260
    current_pvs = len(pvlist)
261
    desired_stripes = params[constants.LDP_STRIPES]
262
    stripes = min(current_pvs, desired_stripes)
263

    
264
    if excl_stor:
265
      (err_msgs, _) = utils.LvmExclusiveCheckNodePvs(pvs_info)
266
      if err_msgs:
267
        for m in err_msgs:
268
          logging.warning(m)
269
      req_pvs = cls._ComputeNumPvs(size, pvs_info)
270
      if spindles:
271
        if spindles < req_pvs:
272
          base.ThrowError("Requested number of spindles (%s) is not enough for"
273
                          " a disk of %d MB (at least %d spindles needed)",
274
                          spindles, size, req_pvs)
275
        else:
276
          req_pvs = spindles
277
      pvlist = cls._GetEmptyPvNames(pvs_info, req_pvs)
278
      current_pvs = len(pvlist)
279
      if current_pvs < req_pvs:
280
        base.ThrowError("Not enough empty PVs (spindles) to create a disk of %d"
281
                        " MB: %d available, %d needed",
282
                        size, current_pvs, req_pvs)
283
      assert current_pvs == len(pvlist)
284
      if stripes > current_pvs:
285
        # No warning issued for this, as it's no surprise
286
        stripes = current_pvs
287

    
288
    else:
289
      if stripes < desired_stripes:
290
        logging.warning("Could not use %d stripes for VG %s, as only %d PVs are"
291
                        " available.", desired_stripes, vg_name, current_pvs)
292
      free_size = sum([pv.free for pv in pvs_info])
293
      # The size constraint should have been checked from the master before
294
      # calling the create function.
295
      if free_size < size:
296
        base.ThrowError("Not enough free space: required %s,"
297
                        " available %s", size, free_size)
298

    
299
    # If the free space is not well distributed, we won't be able to
300
    # create an optimally-striped volume; in that case, we want to try
301
    # with N, N-1, ..., 2, and finally 1 (non-stripped) number of
302
    # stripes
303
    cmd = ["lvcreate", "-L%dm" % size, "-n%s" % lv_name]
304
    for stripes_arg in range(stripes, 0, -1):
305
      result = utils.RunCmd(cmd + ["-i%d" % stripes_arg] + [vg_name] + pvlist)
306
      if not result.failed:
307
        break
308
    if result.failed:
309
      base.ThrowError("LV create failed (%s): %s",
310
                      result.fail_reason, result.output)
311
    return LogicalVolume(unique_id, children, size, params)
312

    
313
  @staticmethod
314
  def _GetVolumeInfo(lvm_cmd, fields):
315
    """Returns LVM Volume infos using lvm_cmd
316

317
    @param lvm_cmd: Should be one of "pvs", "vgs" or "lvs"
318
    @param fields: Fields to return
319
    @return: A list of dicts each with the parsed fields
320

321
    """
322
    if not fields:
323
      raise errors.ProgrammerError("No fields specified")
324

    
325
    sep = "|"
326
    cmd = [lvm_cmd, "--noheadings", "--nosuffix", "--units=m", "--unbuffered",
327
           "--separator=%s" % sep, "-o%s" % ",".join(fields)]
328

    
329
    result = utils.RunCmd(cmd)
330
    if result.failed:
331
      raise errors.CommandError("Can't get the volume information: %s - %s" %
332
                                (result.fail_reason, result.output))
333

    
334
    data = []
335
    for line in result.stdout.splitlines():
336
      splitted_fields = line.strip().split(sep)
337

    
338
      if len(fields) != len(splitted_fields):
339
        raise errors.CommandError("Can't parse %s output: line '%s'" %
340
                                  (lvm_cmd, line))
341

    
342
      data.append(splitted_fields)
343

    
344
    return data
345

    
346
  @classmethod
347
  def GetPVInfo(cls, vg_names, filter_allocatable=True, include_lvs=False):
348
    """Get the free space info for PVs in a volume group.
349

350
    @param vg_names: list of volume group names, if empty all will be returned
351
    @param filter_allocatable: whether to skip over unallocatable PVs
352
    @param include_lvs: whether to include a list of LVs hosted on each PV
353

354
    @rtype: list
355
    @return: list of objects.LvmPvInfo objects
356

357
    """
358
    # We request "lv_name" field only if we care about LVs, so we don't get
359
    # a long list of entries with many duplicates unless we really have to.
360
    # The duplicate "pv_name" field will be ignored.
361
    if include_lvs:
362
      lvfield = "lv_name"
363
    else:
364
      lvfield = "pv_name"
365
    try:
366
      info = cls._GetVolumeInfo("pvs", ["pv_name", "vg_name", "pv_free",
367
                                        "pv_attr", "pv_size", lvfield])
368
    except errors.GenericError, err:
369
      logging.error("Can't get PV information: %s", err)
370
      return None
371

    
372
    # When asked for LVs, "pvs" may return multiple entries for the same PV-LV
373
    # pair. We sort entries by PV name and then LV name, so it's easy to weed
374
    # out duplicates.
375
    if include_lvs:
376
      info.sort(key=(lambda i: (i[0], i[5])))
377
    data = []
378
    lastpvi = None
379
    for (pv_name, vg_name, pv_free, pv_attr, pv_size, lv_name) in info:
380
      # (possibly) skip over pvs which are not allocatable
381
      if filter_allocatable and pv_attr[0] != "a":
382
        continue
383
      # (possibly) skip over pvs which are not in the right volume group(s)
384
      if vg_names and vg_name not in vg_names:
385
        continue
386
      # Beware of duplicates (check before inserting)
387
      if lastpvi and lastpvi.name == pv_name:
388
        if include_lvs and lv_name:
389
          if not lastpvi.lv_list or lastpvi.lv_list[-1] != lv_name:
390
            lastpvi.lv_list.append(lv_name)
391
      else:
392
        if include_lvs and lv_name:
393
          lvl = [lv_name]
394
        else:
395
          lvl = []
396
        lastpvi = objects.LvmPvInfo(name=pv_name, vg_name=vg_name,
397
                                    size=float(pv_size), free=float(pv_free),
398
                                    attributes=pv_attr, lv_list=lvl)
399
        data.append(lastpvi)
400

    
401
    return data
402

    
403
  @classmethod
404
  def _GetExclusiveStorageVgFree(cls, vg_name):
405
    """Return the free disk space in the given VG, in exclusive storage mode.
406

407
    @type vg_name: string
408
    @param vg_name: VG name
409
    @rtype: float
410
    @return: free space in MiB
411
    """
412
    pvs_info = cls.GetPVInfo([vg_name])
413
    if not pvs_info:
414
      return 0.0
415
    pv_size = cls._GetStdPvSize(pvs_info)
416
    num_pvs = len(cls._GetEmptyPvNames(pvs_info))
417
    return pv_size * num_pvs
418

    
419
  @classmethod
420
  def GetVGInfo(cls, vg_names, excl_stor, filter_readonly=True):
421
    """Get the free space info for specific VGs.
422

423
    @param vg_names: list of volume group names, if empty all will be returned
424
    @param excl_stor: whether exclusive_storage is enabled
425
    @param filter_readonly: whether to skip over readonly VGs
426

427
    @rtype: list
428
    @return: list of tuples (free_space, total_size, name) with free_space in
429
             MiB
430

431
    """
432
    try:
433
      info = cls._GetVolumeInfo("vgs", ["vg_name", "vg_free", "vg_attr",
434
                                        "vg_size"])
435
    except errors.GenericError, err:
436
      logging.error("Can't get VG information: %s", err)
437
      return None
438

    
439
    data = []
440
    for vg_name, vg_free, vg_attr, vg_size in info:
441
      # (possibly) skip over vgs which are not writable
442
      if filter_readonly and vg_attr[0] == "r":
443
        continue
444
      # (possibly) skip over vgs which are not in the right volume group(s)
445
      if vg_names and vg_name not in vg_names:
446
        continue
447
      # Exclusive storage needs a different concept of free space
448
      if excl_stor:
449
        es_free = cls._GetExclusiveStorageVgFree(vg_name)
450
        assert es_free <= vg_free
451
        vg_free = es_free
452
      data.append((float(vg_free), float(vg_size), vg_name))
453

    
454
    return data
455

    
456
  @classmethod
457
  def _ValidateName(cls, name):
458
    """Validates that a given name is valid as VG or LV name.
459

460
    The list of valid characters and restricted names is taken out of
461
    the lvm(8) manpage, with the simplification that we enforce both
462
    VG and LV restrictions on the names.
463

464
    """
465
    if (not cls._VALID_NAME_RE.match(name) or
466
        name in cls._INVALID_NAMES or
467
        compat.any(substring in name for substring in cls._INVALID_SUBSTRINGS)):
468
      base.ThrowError("Invalid LVM name '%s'", name)
469

    
470
  def Remove(self):
471
    """Remove this logical volume.
472

473
    """
474
    if not self.minor and not self.Attach():
475
      # the LV does not exist
476
      return
477
    result = utils.RunCmd(["lvremove", "-f", "%s/%s" %
478
                           (self._vg_name, self._lv_name)])
479
    if result.failed:
480
      base.ThrowError("Can't lvremove: %s - %s",
481
                      result.fail_reason, result.output)
482

    
483
  def Rename(self, new_id):
484
    """Rename this logical volume.
485

486
    """
487
    if not isinstance(new_id, (tuple, list)) or len(new_id) != 2:
488
      raise errors.ProgrammerError("Invalid new logical id '%s'" % new_id)
489
    new_vg, new_name = new_id
490
    if new_vg != self._vg_name:
491
      raise errors.ProgrammerError("Can't move a logical volume across"
492
                                   " volume groups (from %s to to %s)" %
493
                                   (self._vg_name, new_vg))
494
    result = utils.RunCmd(["lvrename", new_vg, self._lv_name, new_name])
495
    if result.failed:
496
      base.ThrowError("Failed to rename the logical volume: %s", result.output)
497
    self._lv_name = new_name
498
    self.dev_path = utils.PathJoin("/dev", self._vg_name, self._lv_name)
499

    
500
  @classmethod
501
  def _ParseLvInfoLine(cls, line, sep):
502
    """Parse one line of the lvs output used in L{_GetLvInfo}.
503

504
    """
505
    elems = line.strip().rstrip(sep).split(sep)
506
    if len(elems) != 6:
507
      base.ThrowError("Can't parse LVS output, len(%s) != 6", str(elems))
508

    
509
    (status, major, minor, pe_size, stripes, pvs) = elems
510
    if len(status) < 6:
511
      base.ThrowError("lvs lv_attr is not at least 6 characters (%s)", status)
512

    
513
    try:
514
      major = int(major)
515
      minor = int(minor)
516
    except (TypeError, ValueError), err:
517
      base.ThrowError("lvs major/minor cannot be parsed: %s", str(err))
518

    
519
    try:
520
      pe_size = int(float(pe_size))
521
    except (TypeError, ValueError), err:
522
      base.ThrowError("Can't parse vg extent size: %s", err)
523

    
524
    try:
525
      stripes = int(stripes)
526
    except (TypeError, ValueError), err:
527
      base.ThrowError("Can't parse the number of stripes: %s", err)
528

    
529
    pv_names = []
530
    for pv in pvs.split(","):
531
      m = re.match(cls._PARSE_PV_DEV_RE, pv)
532
      if not m:
533
        base.ThrowError("Can't parse this device list: %s", pvs)
534
      pv_names.append(m.group(1))
535
    assert len(pv_names) > 0
536

    
537
    return (status, major, minor, pe_size, stripes, pv_names)
538

    
539
  @classmethod
540
  def _GetLvInfo(cls, dev_path, _run_cmd=utils.RunCmd):
541
    """Get info about the given existing LV to be used.
542

543
    """
544
    sep = "|"
545
    result = _run_cmd(["lvs", "--noheadings", "--separator=%s" % sep,
546
                       "--units=k", "--nosuffix",
547
                       "-olv_attr,lv_kernel_major,lv_kernel_minor,"
548
                       "vg_extent_size,stripes,devices", dev_path])
549
    if result.failed:
550
      base.ThrowError("Can't find LV %s: %s, %s",
551
                      dev_path, result.fail_reason, result.output)
552
    # the output can (and will) have multiple lines for multi-segment
553
    # LVs, as the 'stripes' parameter is a segment one, so we take
554
    # only the last entry, which is the one we're interested in; note
555
    # that with LVM2 anyway the 'stripes' value must be constant
556
    # across segments, so this is a no-op actually
557
    out = result.stdout.splitlines()
558
    if not out: # totally empty result? splitlines() returns at least
559
                # one line for any non-empty string
560
      base.ThrowError("Can't parse LVS output, no lines? Got '%s'", str(out))
561
    pv_names = set()
562
    for line in out:
563
      (status, major, minor, pe_size, stripes, more_pvs) = \
564
        cls._ParseLvInfoLine(line, sep)
565
      pv_names.update(more_pvs)
566
    return (status, major, minor, pe_size, stripes, pv_names)
567

    
568
  def Attach(self):
569
    """Attach to an existing LV.
570

571
    This method will try to see if an existing and active LV exists
572
    which matches our name. If so, its major/minor will be
573
    recorded.
574

575
    """
576
    self.attached = False
577
    try:
578
      (status, major, minor, pe_size, stripes, pv_names) = \
579
        self._GetLvInfo(self.dev_path)
580
    except errors.BlockDeviceError:
581
      return False
582

    
583
    self.major = major
584
    self.minor = minor
585
    self.pe_size = pe_size
586
    self.stripe_count = stripes
587
    self._degraded = status[0] == "v" # virtual volume, i.e. doesn't backing
588
                                      # storage
589
    self.pv_names = pv_names
590
    self.attached = True
591
    return True
592

    
593
  def Assemble(self):
594
    """Assemble the device.
595

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

600
    """
601
    result = utils.RunCmd(["lvchange", "-ay", self.dev_path])
602
    if result.failed:
603
      base.ThrowError("Can't activate lv %s: %s", self.dev_path, result.output)
604

    
605
  def Shutdown(self):
606
    """Shutdown the device.
607

608
    This is a no-op for the LV device type, as we don't deactivate the
609
    volumes on shutdown.
610

611
    """
612
    pass
613

    
614
  def GetSyncStatus(self):
615
    """Returns the sync status of the device.
616

617
    If this device is a mirroring device, this function returns the
618
    status of the mirror.
619

620
    For logical volumes, sync_percent and estimated_time are always
621
    None (no recovery in progress, as we don't handle the mirrored LV
622
    case). The is_degraded parameter is the inverse of the ldisk
623
    parameter.
624

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

631
    The status was already read in Attach, so we just return it.
632

633
    @rtype: objects.BlockDevStatus
634

635
    """
636
    if self._degraded:
637
      ldisk_status = constants.LDS_FAULTY
638
    else:
639
      ldisk_status = constants.LDS_OKAY
640

    
641
    return objects.BlockDevStatus(dev_path=self.dev_path,
642
                                  major=self.major,
643
                                  minor=self.minor,
644
                                  sync_percent=None,
645
                                  estimated_time=None,
646
                                  is_degraded=self._degraded,
647
                                  ldisk_status=ldisk_status)
648

    
649
  def Open(self, force=False):
650
    """Make the device ready for I/O.
651

652
    This is a no-op for the LV device type.
653

654
    """
655
    pass
656

    
657
  def Close(self):
658
    """Notifies that the device will no longer be used for I/O.
659

660
    This is a no-op for the LV device type.
661

662
    """
663
    pass
664

    
665
  def Snapshot(self, size):
666
    """Create a snapshot copy of an lvm block device.
667

668
    @returns: tuple (vg, lv)
669

670
    """
671
    snap_name = self._lv_name + ".snap"
672

    
673
    # remove existing snapshot if found
674
    snap = LogicalVolume((self._vg_name, snap_name), None, size, self.params)
675
    base.IgnoreError(snap.Remove)
676

    
677
    vg_info = self.GetVGInfo([self._vg_name], False)
678
    if not vg_info:
679
      base.ThrowError("Can't compute VG info for vg %s", self._vg_name)
680
    free_size, _, _ = vg_info[0]
681
    if free_size < size:
682
      base.ThrowError("Not enough free space: required %s,"
683
                      " available %s", size, free_size)
684

    
685
    _CheckResult(utils.RunCmd(["lvcreate", "-L%dm" % size, "-s",
686
                               "-n%s" % snap_name, self.dev_path]))
687

    
688
    return (self._vg_name, snap_name)
689

    
690
  def _RemoveOldInfo(self):
691
    """Try to remove old tags from the lv.
692

693
    """
694
    result = utils.RunCmd(["lvs", "-o", "tags", "--noheadings", "--nosuffix",
695
                           self.dev_path])
696
    _CheckResult(result)
697

    
698
    raw_tags = result.stdout.strip()
699
    if raw_tags:
700
      for tag in raw_tags.split(","):
701
        _CheckResult(utils.RunCmd(["lvchange", "--deltag",
702
                                   tag.strip(), self.dev_path]))
703

    
704
  def SetInfo(self, text):
705
    """Update metadata with info text.
706

707
    """
708
    base.BlockDev.SetInfo(self, text)
709

    
710
    self._RemoveOldInfo()
711

    
712
    # Replace invalid characters
713
    text = re.sub("^[^A-Za-z0-9_+.]", "_", text)
714
    text = re.sub("[^-A-Za-z0-9_+.]", "_", text)
715

    
716
    # Only up to 128 characters are allowed
717
    text = text[:128]
718

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

    
721
  def Grow(self, amount, dryrun, backingstore):
722
    """Grow the logical volume.
723

724
    """
725
    if not backingstore:
726
      return
727
    if self.pe_size is None or self.stripe_count is None:
728
      if not self.Attach():
729
        base.ThrowError("Can't attach to LV during Grow()")
730
    full_stripe_size = self.pe_size * self.stripe_count
731
    # pe_size is in KB
732
    amount *= 1024
733
    rest = amount % full_stripe_size
734
    if rest != 0:
735
      amount += full_stripe_size - rest
736
    cmd = ["lvextend", "-L", "+%dk" % amount]
737
    if dryrun:
738
      cmd.append("--test")
739
    # we try multiple algorithms since the 'best' ones might not have
740
    # space available in the right place, but later ones might (since
741
    # they have less constraints); also note that only recent LVM
742
    # supports 'cling'
743
    for alloc_policy in "contiguous", "cling", "normal":
744
      result = utils.RunCmd(cmd + ["--alloc", alloc_policy, self.dev_path])
745
      if not result.failed:
746
        return
747
    base.ThrowError("Can't grow LV %s: %s", self.dev_path, result.output)
748

    
749

    
750
class FileStorage(base.BlockDev):
751
  """File device.
752

753
  This class represents the a file storage backend device.
754

755
  The unique_id for the file device is a (file_driver, file_path) tuple.
756

757
  """
758
  def __init__(self, unique_id, children, size, params):
759
    """Initalizes a file device backend.
760

761
    """
762
    if children:
763
      raise errors.BlockDeviceError("Invalid setup for file device")
764
    super(FileStorage, self).__init__(unique_id, children, size, params)
765
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
766
      raise ValueError("Invalid configuration data %s" % str(unique_id))
767
    self.driver = unique_id[0]
768
    self.dev_path = unique_id[1]
769

    
770
    CheckFileStoragePath(self.dev_path)
771

    
772
    self.Attach()
773

    
774
  def Assemble(self):
775
    """Assemble the device.
776

777
    Checks whether the file device exists, raises BlockDeviceError otherwise.
778

779
    """
780
    if not os.path.exists(self.dev_path):
781
      base.ThrowError("File device '%s' does not exist" % self.dev_path)
782

    
783
  def Shutdown(self):
784
    """Shutdown the device.
785

786
    This is a no-op for the file type, as we don't deactivate
787
    the file on shutdown.
788

789
    """
790
    pass
791

    
792
  def Open(self, force=False):
793
    """Make the device ready for I/O.
794

795
    This is a no-op for the file type.
796

797
    """
798
    pass
799

    
800
  def Close(self):
801
    """Notifies that the device will no longer be used for I/O.
802

803
    This is a no-op for the file type.
804

805
    """
806
    pass
807

    
808
  def Remove(self):
809
    """Remove the file backing the block device.
810

811
    @rtype: boolean
812
    @return: True if the removal was successful
813

814
    """
815
    try:
816
      os.remove(self.dev_path)
817
    except OSError, err:
818
      if err.errno != errno.ENOENT:
819
        base.ThrowError("Can't remove file '%s': %s", self.dev_path, err)
820

    
821
  def Rename(self, new_id):
822
    """Renames the file.
823

824
    """
825
    # TODO: implement rename for file-based storage
826
    base.ThrowError("Rename is not supported for file-based storage")
827

    
828
  def Grow(self, amount, dryrun, backingstore):
829
    """Grow the file
830

831
    @param amount: the amount (in mebibytes) to grow with
832

833
    """
834
    if not backingstore:
835
      return
836
    # Check that the file exists
837
    self.Assemble()
838
    current_size = self.GetActualSize()
839
    new_size = current_size + amount * 1024 * 1024
840
    assert new_size > current_size, "Cannot Grow with a negative amount"
841
    # We can't really simulate the growth
842
    if dryrun:
843
      return
844
    try:
845
      f = open(self.dev_path, "a+")
846
      f.truncate(new_size)
847
      f.close()
848
    except EnvironmentError, err:
849
      base.ThrowError("Error in file growth: %", str(err))
850

    
851
  def Attach(self):
852
    """Attach to an existing file.
853

854
    Check if this file already exists.
855

856
    @rtype: boolean
857
    @return: True if file exists
858

859
    """
860
    self.attached = os.path.exists(self.dev_path)
861
    return self.attached
862

    
863
  def GetActualSize(self):
864
    """Return the actual disk size.
865

866
    @note: the device needs to be active when this is called
867

868
    """
869
    assert self.attached, "BlockDevice not attached in GetActualSize()"
870
    try:
871
      st = os.stat(self.dev_path)
872
      return st.st_size
873
    except OSError, err:
874
      base.ThrowError("Can't stat %s: %s", self.dev_path, err)
875

    
876
  @classmethod
877
  def Create(cls, unique_id, children, size, spindles, params, excl_stor):
878
    """Create a new file.
879

880
    @param size: the size of file in MiB
881

882
    @rtype: L{bdev.FileStorage}
883
    @return: an instance of FileStorage
884

885
    """
886
    if excl_stor:
887
      raise errors.ProgrammerError("FileStorage device requested with"
888
                                   " exclusive_storage")
889
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
890
      raise ValueError("Invalid configuration data %s" % str(unique_id))
891

    
892
    dev_path = unique_id[1]
893

    
894
    CheckFileStoragePath(dev_path)
895

    
896
    try:
897
      fd = os.open(dev_path, os.O_RDWR | os.O_CREAT | os.O_EXCL)
898
      f = os.fdopen(fd, "w")
899
      f.truncate(size * 1024 * 1024)
900
      f.close()
901
    except EnvironmentError, err:
902
      if err.errno == errno.EEXIST:
903
        base.ThrowError("File already existing: %s", dev_path)
904
      base.ThrowError("Error in file creation: %", str(err))
905

    
906
    return FileStorage(unique_id, children, size, params)
907

    
908

    
909
class PersistentBlockDevice(base.BlockDev):
910
  """A block device with persistent node
911

912
  May be either directly attached, or exposed through DM (e.g. dm-multipath).
913
  udev helpers are probably required to give persistent, human-friendly
914
  names.
915

916
  For the time being, pathnames are required to lie under /dev.
917

918
  """
919
  def __init__(self, unique_id, children, size, params):
920
    """Attaches to a static block device.
921

922
    The unique_id is a path under /dev.
923

924
    """
925
    super(PersistentBlockDevice, self).__init__(unique_id, children, size,
926
                                                params)
927
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
928
      raise ValueError("Invalid configuration data %s" % str(unique_id))
929
    self.dev_path = unique_id[1]
930
    if not os.path.realpath(self.dev_path).startswith("/dev/"):
931
      raise ValueError("Full path '%s' lies outside /dev" %
932
                              os.path.realpath(self.dev_path))
933
    # TODO: this is just a safety guard checking that we only deal with devices
934
    # we know how to handle. In the future this will be integrated with
935
    # external storage backends and possible values will probably be collected
936
    # from the cluster configuration.
937
    if unique_id[0] != constants.BLOCKDEV_DRIVER_MANUAL:
938
      raise ValueError("Got persistent block device of invalid type: %s" %
939
                       unique_id[0])
940

    
941
    self.major = self.minor = None
942
    self.Attach()
943

    
944
  @classmethod
945
  def Create(cls, unique_id, children, size, spindles, params, excl_stor):
946
    """Create a new device
947

948
    This is a noop, we only return a PersistentBlockDevice instance
949

950
    """
951
    if excl_stor:
952
      raise errors.ProgrammerError("Persistent block device requested with"
953
                                   " exclusive_storage")
954
    return PersistentBlockDevice(unique_id, children, 0, params)
955

    
956
  def Remove(self):
957
    """Remove a device
958

959
    This is a noop
960

961
    """
962
    pass
963

    
964
  def Rename(self, new_id):
965
    """Rename this device.
966

967
    """
968
    base.ThrowError("Rename is not supported for PersistentBlockDev storage")
969

    
970
  def Attach(self):
971
    """Attach to an existing block device.
972

973

974
    """
975
    self.attached = False
976
    try:
977
      st = os.stat(self.dev_path)
978
    except OSError, err:
979
      logging.error("Error stat()'ing %s: %s", self.dev_path, str(err))
980
      return False
981

    
982
    if not stat.S_ISBLK(st.st_mode):
983
      logging.error("%s is not a block device", self.dev_path)
984
      return False
985

    
986
    self.major = os.major(st.st_rdev)
987
    self.minor = os.minor(st.st_rdev)
988
    self.attached = True
989

    
990
    return True
991

    
992
  def Assemble(self):
993
    """Assemble the device.
994

995
    """
996
    pass
997

    
998
  def Shutdown(self):
999
    """Shutdown the device.
1000

1001
    """
1002
    pass
1003

    
1004
  def Open(self, force=False):
1005
    """Make the device ready for I/O.
1006

1007
    """
1008
    pass
1009

    
1010
  def Close(self):
1011
    """Notifies that the device will no longer be used for I/O.
1012

1013
    """
1014
    pass
1015

    
1016
  def Grow(self, amount, dryrun, backingstore):
1017
    """Grow the logical volume.
1018

1019
    """
1020
    base.ThrowError("Grow is not supported for PersistentBlockDev storage")
1021

    
1022

    
1023
class RADOSBlockDevice(base.BlockDev):
1024
  """A RADOS Block Device (rbd).
1025

1026
  This class implements the RADOS Block Device for the backend. You need
1027
  the rbd kernel driver, the RADOS Tools and a working RADOS cluster for
1028
  this to be functional.
1029

1030
  """
1031
  def __init__(self, unique_id, children, size, params):
1032
    """Attaches to an rbd device.
1033

1034
    """
1035
    super(RADOSBlockDevice, self).__init__(unique_id, children, size, params)
1036
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1037
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1038

    
1039
    self.driver, self.rbd_name = unique_id
1040

    
1041
    self.major = self.minor = None
1042
    self.Attach()
1043

    
1044
  @classmethod
1045
  def Create(cls, unique_id, children, size, spindles, params, excl_stor):
1046
    """Create a new rbd device.
1047

1048
    Provision a new rbd volume inside a RADOS pool.
1049

1050
    """
1051
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1052
      raise errors.ProgrammerError("Invalid configuration data %s" %
1053
                                   str(unique_id))
1054
    if excl_stor:
1055
      raise errors.ProgrammerError("RBD device requested with"
1056
                                   " exclusive_storage")
1057
    rbd_pool = params[constants.LDP_POOL]
1058
    rbd_name = unique_id[1]
1059

    
1060
    # Provision a new rbd volume (Image) inside the RADOS cluster.
1061
    cmd = [constants.RBD_CMD, "create", "-p", rbd_pool,
1062
           rbd_name, "--size", "%s" % size]
1063
    result = utils.RunCmd(cmd)
1064
    if result.failed:
1065
      base.ThrowError("rbd creation failed (%s): %s",
1066
                      result.fail_reason, result.output)
1067

    
1068
    return RADOSBlockDevice(unique_id, children, size, params)
1069

    
1070
  def Remove(self):
1071
    """Remove the rbd device.
1072

1073
    """
1074
    rbd_pool = self.params[constants.LDP_POOL]
1075
    rbd_name = self.unique_id[1]
1076

    
1077
    if not self.minor and not self.Attach():
1078
      # The rbd device doesn't exist.
1079
      return
1080

    
1081
    # First shutdown the device (remove mappings).
1082
    self.Shutdown()
1083

    
1084
    # Remove the actual Volume (Image) from the RADOS cluster.
1085
    cmd = [constants.RBD_CMD, "rm", "-p", rbd_pool, rbd_name]
1086
    result = utils.RunCmd(cmd)
1087
    if result.failed:
1088
      base.ThrowError("Can't remove Volume from cluster with rbd rm: %s - %s",
1089
                      result.fail_reason, result.output)
1090

    
1091
  def Rename(self, new_id):
1092
    """Rename this device.
1093

1094
    """
1095
    pass
1096

    
1097
  def Attach(self):
1098
    """Attach to an existing rbd device.
1099

1100
    This method maps the rbd volume that matches our name with
1101
    an rbd device and then attaches to this device.
1102

1103
    """
1104
    self.attached = False
1105

    
1106
    # Map the rbd volume to a block device under /dev
1107
    self.dev_path = self._MapVolumeToBlockdev(self.unique_id)
1108

    
1109
    try:
1110
      st = os.stat(self.dev_path)
1111
    except OSError, err:
1112
      logging.error("Error stat()'ing %s: %s", self.dev_path, str(err))
1113
      return False
1114

    
1115
    if not stat.S_ISBLK(st.st_mode):
1116
      logging.error("%s is not a block device", self.dev_path)
1117
      return False
1118

    
1119
    self.major = os.major(st.st_rdev)
1120
    self.minor = os.minor(st.st_rdev)
1121
    self.attached = True
1122

    
1123
    return True
1124

    
1125
  def _MapVolumeToBlockdev(self, unique_id):
1126
    """Maps existing rbd volumes to block devices.
1127

1128
    This method should be idempotent if the mapping already exists.
1129

1130
    @rtype: string
1131
    @return: the block device path that corresponds to the volume
1132

1133
    """
1134
    pool = self.params[constants.LDP_POOL]
1135
    name = unique_id[1]
1136

    
1137
    # Check if the mapping already exists.
1138
    rbd_dev = self._VolumeToBlockdev(pool, name)
1139
    if rbd_dev:
1140
      # The mapping exists. Return it.
1141
      return rbd_dev
1142

    
1143
    # The mapping doesn't exist. Create it.
1144
    map_cmd = [constants.RBD_CMD, "map", "-p", pool, name]
1145
    result = utils.RunCmd(map_cmd)
1146
    if result.failed:
1147
      base.ThrowError("rbd map failed (%s): %s",
1148
                      result.fail_reason, result.output)
1149

    
1150
    # Find the corresponding rbd device.
1151
    rbd_dev = self._VolumeToBlockdev(pool, name)
1152
    if not rbd_dev:
1153
      base.ThrowError("rbd map succeeded, but could not find the rbd block"
1154
                      " device in output of showmapped, for volume: %s", name)
1155

    
1156
    # The device was successfully mapped. Return it.
1157
    return rbd_dev
1158

    
1159
  @classmethod
1160
  def _VolumeToBlockdev(cls, pool, volume_name):
1161
    """Do the 'volume name'-to-'rbd block device' resolving.
1162

1163
    @type pool: string
1164
    @param pool: RADOS pool to use
1165
    @type volume_name: string
1166
    @param volume_name: the name of the volume whose device we search for
1167
    @rtype: string or None
1168
    @return: block device path if the volume is mapped, else None
1169

1170
    """
1171
    try:
1172
      # Newer versions of the rbd tool support json output formatting. Use it
1173
      # if available.
1174
      showmap_cmd = [
1175
        constants.RBD_CMD,
1176
        "showmapped",
1177
        "-p",
1178
        pool,
1179
        "--format",
1180
        "json"
1181
        ]
1182
      result = utils.RunCmd(showmap_cmd)
1183
      if result.failed:
1184
        logging.error("rbd JSON output formatting returned error (%s): %s,"
1185
                      "falling back to plain output parsing",
1186
                      result.fail_reason, result.output)
1187
        raise RbdShowmappedJsonError
1188

    
1189
      return cls._ParseRbdShowmappedJson(result.output, volume_name)
1190
    except RbdShowmappedJsonError:
1191
      # For older versions of rbd, we have to parse the plain / text output
1192
      # manually.
1193
      showmap_cmd = [constants.RBD_CMD, "showmapped", "-p", pool]
1194
      result = utils.RunCmd(showmap_cmd)
1195
      if result.failed:
1196
        base.ThrowError("rbd showmapped failed (%s): %s",
1197
                        result.fail_reason, result.output)
1198

    
1199
      return cls._ParseRbdShowmappedPlain(result.output, volume_name)
1200

    
1201
  @staticmethod
1202
  def _ParseRbdShowmappedJson(output, volume_name):
1203
    """Parse the json output of `rbd showmapped'.
1204

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

1208
    @type output: string
1209
    @param output: the json output of `rbd showmapped'
1210
    @type volume_name: string
1211
    @param volume_name: the name of the volume whose device we search for
1212
    @rtype: string or None
1213
    @return: block device path if the volume is mapped, else None
1214

1215
    """
1216
    try:
1217
      devices = serializer.LoadJson(output)
1218
    except ValueError, err:
1219
      base.ThrowError("Unable to parse JSON data: %s" % err)
1220

    
1221
    rbd_dev = None
1222
    for d in devices.values(): # pylint: disable=E1103
1223
      try:
1224
        name = d["name"]
1225
      except KeyError:
1226
        base.ThrowError("'name' key missing from json object %s", devices)
1227

    
1228
      if name == volume_name:
1229
        if rbd_dev is not None:
1230
          base.ThrowError("rbd volume %s is mapped more than once", volume_name)
1231

    
1232
        rbd_dev = d["device"]
1233

    
1234
    return rbd_dev
1235

    
1236
  @staticmethod
1237
  def _ParseRbdShowmappedPlain(output, volume_name):
1238
    """Parse the (plain / text) output of `rbd showmapped'.
1239

1240
    This method parses the output of `rbd showmapped' and returns
1241
    the rbd block device path (e.g. /dev/rbd0) that matches the
1242
    given rbd volume.
1243

1244
    @type output: string
1245
    @param output: the plain text output of `rbd showmapped'
1246
    @type volume_name: string
1247
    @param volume_name: the name of the volume whose device we search for
1248
    @rtype: string or None
1249
    @return: block device path if the volume is mapped, else None
1250

1251
    """
1252
    allfields = 5
1253
    volumefield = 2
1254
    devicefield = 4
1255

    
1256
    lines = output.splitlines()
1257

    
1258
    # Try parsing the new output format (ceph >= 0.55).
1259
    splitted_lines = map(lambda l: l.split(), lines)
1260

    
1261
    # Check for empty output.
1262
    if not splitted_lines:
1263
      return None
1264

    
1265
    # Check showmapped output, to determine number of fields.
1266
    field_cnt = len(splitted_lines[0])
1267
    if field_cnt != allfields:
1268
      # Parsing the new format failed. Fallback to parsing the old output
1269
      # format (< 0.55).
1270
      splitted_lines = map(lambda l: l.split("\t"), lines)
1271
      if field_cnt != allfields:
1272
        base.ThrowError("Cannot parse rbd showmapped output expected %s fields,"
1273
                        " found %s", allfields, field_cnt)
1274

    
1275
    matched_lines = \
1276
      filter(lambda l: len(l) == allfields and l[volumefield] == volume_name,
1277
             splitted_lines)
1278

    
1279
    if len(matched_lines) > 1:
1280
      base.ThrowError("rbd volume %s mapped more than once", volume_name)
1281

    
1282
    if matched_lines:
1283
      # rbd block device found. Return it.
1284
      rbd_dev = matched_lines[0][devicefield]
1285
      return rbd_dev
1286

    
1287
    # The given volume is not mapped.
1288
    return None
1289

    
1290
  def Assemble(self):
1291
    """Assemble the device.
1292

1293
    """
1294
    pass
1295

    
1296
  def Shutdown(self):
1297
    """Shutdown the device.
1298

1299
    """
1300
    if not self.minor and not self.Attach():
1301
      # The rbd device doesn't exist.
1302
      return
1303

    
1304
    # Unmap the block device from the Volume.
1305
    self._UnmapVolumeFromBlockdev(self.unique_id)
1306

    
1307
    self.minor = None
1308
    self.dev_path = None
1309

    
1310
  def _UnmapVolumeFromBlockdev(self, unique_id):
1311
    """Unmaps the rbd device from the Volume it is mapped.
1312

1313
    Unmaps the rbd device from the Volume it was previously mapped to.
1314
    This method should be idempotent if the Volume isn't mapped.
1315

1316
    """
1317
    pool = self.params[constants.LDP_POOL]
1318
    name = unique_id[1]
1319

    
1320
    # Check if the mapping already exists.
1321
    rbd_dev = self._VolumeToBlockdev(pool, name)
1322

    
1323
    if rbd_dev:
1324
      # The mapping exists. Unmap the rbd device.
1325
      unmap_cmd = [constants.RBD_CMD, "unmap", "%s" % rbd_dev]
1326
      result = utils.RunCmd(unmap_cmd)
1327
      if result.failed:
1328
        base.ThrowError("rbd unmap failed (%s): %s",
1329
                        result.fail_reason, result.output)
1330

    
1331
  def Open(self, force=False):
1332
    """Make the device ready for I/O.
1333

1334
    """
1335
    pass
1336

    
1337
  def Close(self):
1338
    """Notifies that the device will no longer be used for I/O.
1339

1340
    """
1341
    pass
1342

    
1343
  def Grow(self, amount, dryrun, backingstore):
1344
    """Grow the Volume.
1345

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

1352
    """
1353
    if not backingstore:
1354
      return
1355
    if not self.Attach():
1356
      base.ThrowError("Can't attach to rbd device during Grow()")
1357

    
1358
    if dryrun:
1359
      # the rbd tool does not support dry runs of resize operations.
1360
      # Since rbd volumes are thinly provisioned, we assume
1361
      # there is always enough free space for the operation.
1362
      return
1363

    
1364
    rbd_pool = self.params[constants.LDP_POOL]
1365
    rbd_name = self.unique_id[1]
1366
    new_size = self.size + amount
1367

    
1368
    # Resize the rbd volume (Image) inside the RADOS cluster.
1369
    cmd = [constants.RBD_CMD, "resize", "-p", rbd_pool,
1370
           rbd_name, "--size", "%s" % new_size]
1371
    result = utils.RunCmd(cmd)
1372
    if result.failed:
1373
      base.ThrowError("rbd resize failed (%s): %s",
1374
                      result.fail_reason, result.output)
1375

    
1376

    
1377
class ExtStorageDevice(base.BlockDev):
1378
  """A block device provided by an ExtStorage Provider.
1379

1380
  This class implements the External Storage Interface, which means
1381
  handling of the externally provided block devices.
1382

1383
  """
1384
  def __init__(self, unique_id, children, size, params):
1385
    """Attaches to an extstorage block device.
1386

1387
    """
1388
    super(ExtStorageDevice, self).__init__(unique_id, children, size, params)
1389
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1390
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1391

    
1392
    self.driver, self.vol_name = unique_id
1393
    self.ext_params = params
1394

    
1395
    self.major = self.minor = None
1396
    self.Attach()
1397

    
1398
  @classmethod
1399
  def Create(cls, unique_id, children, size, spindles, params, excl_stor):
1400
    """Create a new extstorage device.
1401

1402
    Provision a new volume using an extstorage provider, which will
1403
    then be mapped to a block device.
1404

1405
    """
1406
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1407
      raise errors.ProgrammerError("Invalid configuration data %s" %
1408
                                   str(unique_id))
1409
    if excl_stor:
1410
      raise errors.ProgrammerError("extstorage device requested with"
1411
                                   " exclusive_storage")
1412

    
1413
    # Call the External Storage's create script,
1414
    # to provision a new Volume inside the External Storage
1415
    _ExtStorageAction(constants.ES_ACTION_CREATE, unique_id,
1416
                      params, str(size))
1417

    
1418
    return ExtStorageDevice(unique_id, children, size, params)
1419

    
1420
  def Remove(self):
1421
    """Remove the extstorage device.
1422

1423
    """
1424
    if not self.minor and not self.Attach():
1425
      # The extstorage device doesn't exist.
1426
      return
1427

    
1428
    # First shutdown the device (remove mappings).
1429
    self.Shutdown()
1430

    
1431
    # Call the External Storage's remove script,
1432
    # to remove the Volume from the External Storage
1433
    _ExtStorageAction(constants.ES_ACTION_REMOVE, self.unique_id,
1434
                      self.ext_params)
1435

    
1436
  def Rename(self, new_id):
1437
    """Rename this device.
1438

1439
    """
1440
    pass
1441

    
1442
  def Attach(self):
1443
    """Attach to an existing extstorage device.
1444

1445
    This method maps the extstorage volume that matches our name with
1446
    a corresponding block device and then attaches to this device.
1447

1448
    """
1449
    self.attached = False
1450

    
1451
    # Call the External Storage's attach script,
1452
    # to attach an existing Volume to a block device under /dev
1453
    self.dev_path = _ExtStorageAction(constants.ES_ACTION_ATTACH,
1454
                                      self.unique_id, self.ext_params)
1455

    
1456
    try:
1457
      st = os.stat(self.dev_path)
1458
    except OSError, err:
1459
      logging.error("Error stat()'ing %s: %s", self.dev_path, str(err))
1460
      return False
1461

    
1462
    if not stat.S_ISBLK(st.st_mode):
1463
      logging.error("%s is not a block device", self.dev_path)
1464
      return False
1465

    
1466
    self.major = os.major(st.st_rdev)
1467
    self.minor = os.minor(st.st_rdev)
1468
    self.attached = True
1469

    
1470
    return True
1471

    
1472
  def Assemble(self):
1473
    """Assemble the device.
1474

1475
    """
1476
    pass
1477

    
1478
  def Shutdown(self):
1479
    """Shutdown the device.
1480

1481
    """
1482
    if not self.minor and not self.Attach():
1483
      # The extstorage device doesn't exist.
1484
      return
1485

    
1486
    # Call the External Storage's detach script,
1487
    # to detach an existing Volume from it's block device under /dev
1488
    _ExtStorageAction(constants.ES_ACTION_DETACH, self.unique_id,
1489
                      self.ext_params)
1490

    
1491
    self.minor = None
1492
    self.dev_path = None
1493

    
1494
  def Open(self, force=False):
1495
    """Make the device ready for I/O.
1496

1497
    """
1498
    pass
1499

    
1500
  def Close(self):
1501
    """Notifies that the device will no longer be used for I/O.
1502

1503
    """
1504
    pass
1505

    
1506
  def Grow(self, amount, dryrun, backingstore):
1507
    """Grow the Volume.
1508

1509
    @type amount: integer
1510
    @param amount: the amount (in mebibytes) to grow with
1511
    @type dryrun: boolean
1512
    @param dryrun: whether to execute the operation in simulation mode
1513
        only, without actually increasing the size
1514

1515
    """
1516
    if not backingstore:
1517
      return
1518
    if not self.Attach():
1519
      base.ThrowError("Can't attach to extstorage device during Grow()")
1520

    
1521
    if dryrun:
1522
      # we do not support dry runs of resize operations for now.
1523
      return
1524

    
1525
    new_size = self.size + amount
1526

    
1527
    # Call the External Storage's grow script,
1528
    # to grow an existing Volume inside the External Storage
1529
    _ExtStorageAction(constants.ES_ACTION_GROW, self.unique_id,
1530
                      self.ext_params, str(self.size), grow=str(new_size))
1531

    
1532
  def SetInfo(self, text):
1533
    """Update metadata with info text.
1534

1535
    """
1536
    # Replace invalid characters
1537
    text = re.sub("^[^A-Za-z0-9_+.]", "_", text)
1538
    text = re.sub("[^-A-Za-z0-9_+.]", "_", text)
1539

    
1540
    # Only up to 128 characters are allowed
1541
    text = text[:128]
1542

    
1543
    # Call the External Storage's setinfo script,
1544
    # to set metadata for an existing Volume inside the External Storage
1545
    _ExtStorageAction(constants.ES_ACTION_SETINFO, self.unique_id,
1546
                      self.ext_params, metadata=text)
1547

    
1548

    
1549
def _ExtStorageAction(action, unique_id, ext_params,
1550
                      size=None, grow=None, metadata=None):
1551
  """Take an External Storage action.
1552

1553
  Take an External Storage action concerning or affecting
1554
  a specific Volume inside the External Storage.
1555

1556
  @type action: string
1557
  @param action: which action to perform. One of:
1558
                 create / remove / grow / attach / detach
1559
  @type unique_id: tuple (driver, vol_name)
1560
  @param unique_id: a tuple containing the type of ExtStorage (driver)
1561
                    and the Volume name
1562
  @type ext_params: dict
1563
  @param ext_params: ExtStorage parameters
1564
  @type size: integer
1565
  @param size: the size of the Volume in mebibytes
1566
  @type grow: integer
1567
  @param grow: the new size in mebibytes (after grow)
1568
  @type metadata: string
1569
  @param metadata: metadata info of the Volume, for use by the provider
1570
  @rtype: None or a block device path (during attach)
1571

1572
  """
1573
  driver, vol_name = unique_id
1574

    
1575
  # Create an External Storage instance of type `driver'
1576
  status, inst_es = ExtStorageFromDisk(driver)
1577
  if not status:
1578
    base.ThrowError("%s" % inst_es)
1579

    
1580
  # Create the basic environment for the driver's scripts
1581
  create_env = _ExtStorageEnvironment(unique_id, ext_params, size,
1582
                                      grow, metadata)
1583

    
1584
  # Do not use log file for action `attach' as we need
1585
  # to get the output from RunResult
1586
  # TODO: find a way to have a log file for attach too
1587
  logfile = None
1588
  if action is not constants.ES_ACTION_ATTACH:
1589
    logfile = _VolumeLogName(action, driver, vol_name)
1590

    
1591
  # Make sure the given action results in a valid script
1592
  if action not in constants.ES_SCRIPTS:
1593
    base.ThrowError("Action '%s' doesn't result in a valid ExtStorage script" %
1594
                    action)
1595

    
1596
  # Find out which external script to run according the given action
1597
  script_name = action + "_script"
1598
  script = getattr(inst_es, script_name)
1599

    
1600
  # Run the external script
1601
  result = utils.RunCmd([script], env=create_env,
1602
                        cwd=inst_es.path, output=logfile,)
1603
  if result.failed:
1604
    logging.error("External storage's %s command '%s' returned"
1605
                  " error: %s, logfile: %s, output: %s",
1606
                  action, result.cmd, result.fail_reason,
1607
                  logfile, result.output)
1608

    
1609
    # If logfile is 'None' (during attach), it breaks TailFile
1610
    # TODO: have a log file for attach too
1611
    if action is not constants.ES_ACTION_ATTACH:
1612
      lines = [utils.SafeEncode(val)
1613
               for val in utils.TailFile(logfile, lines=20)]
1614
    else:
1615
      lines = result.output[-20:]
1616

    
1617
    base.ThrowError("External storage's %s script failed (%s), last"
1618
                    " lines of output:\n%s",
1619
                    action, result.fail_reason, "\n".join(lines))
1620

    
1621
  if action == constants.ES_ACTION_ATTACH:
1622
    return result.stdout
1623

    
1624

    
1625
def ExtStorageFromDisk(name, base_dir=None):
1626
  """Create an ExtStorage instance from disk.
1627

1628
  This function will return an ExtStorage instance
1629
  if the given name is a valid ExtStorage name.
1630

1631
  @type base_dir: string
1632
  @keyword base_dir: Base directory containing ExtStorage installations.
1633
                     Defaults to a search in all the ES_SEARCH_PATH dirs.
1634
  @rtype: tuple
1635
  @return: True and the ExtStorage instance if we find a valid one, or
1636
      False and the diagnose message on error
1637

1638
  """
1639
  if base_dir is None:
1640
    es_base_dir = pathutils.ES_SEARCH_PATH
1641
  else:
1642
    es_base_dir = [base_dir]
1643

    
1644
  es_dir = utils.FindFile(name, es_base_dir, os.path.isdir)
1645

    
1646
  if es_dir is None:
1647
    return False, ("Directory for External Storage Provider %s not"
1648
                   " found in search path" % name)
1649

    
1650
  # ES Files dictionary, we will populate it with the absolute path
1651
  # names; if the value is True, then it is a required file, otherwise
1652
  # an optional one
1653
  es_files = dict.fromkeys(constants.ES_SCRIPTS, True)
1654

    
1655
  es_files[constants.ES_PARAMETERS_FILE] = True
1656

    
1657
  for (filename, _) in es_files.items():
1658
    es_files[filename] = utils.PathJoin(es_dir, filename)
1659

    
1660
    try:
1661
      st = os.stat(es_files[filename])
1662
    except EnvironmentError, err:
1663
      return False, ("File '%s' under path '%s' is missing (%s)" %
1664
                     (filename, es_dir, utils.ErrnoOrStr(err)))
1665

    
1666
    if not stat.S_ISREG(stat.S_IFMT(st.st_mode)):
1667
      return False, ("File '%s' under path '%s' is not a regular file" %
1668
                     (filename, es_dir))
1669

    
1670
    if filename in constants.ES_SCRIPTS:
1671
      if stat.S_IMODE(st.st_mode) & stat.S_IXUSR != stat.S_IXUSR:
1672
        return False, ("File '%s' under path '%s' is not executable" %
1673
                       (filename, es_dir))
1674

    
1675
  parameters = []
1676
  if constants.ES_PARAMETERS_FILE in es_files:
1677
    parameters_file = es_files[constants.ES_PARAMETERS_FILE]
1678
    try:
1679
      parameters = utils.ReadFile(parameters_file).splitlines()
1680
    except EnvironmentError, err:
1681
      return False, ("Error while reading the EXT parameters file at %s: %s" %
1682
                     (parameters_file, utils.ErrnoOrStr(err)))
1683
    parameters = [v.split(None, 1) for v in parameters]
1684

    
1685
  es_obj = \
1686
    objects.ExtStorage(name=name, path=es_dir,
1687
                       create_script=es_files[constants.ES_SCRIPT_CREATE],
1688
                       remove_script=es_files[constants.ES_SCRIPT_REMOVE],
1689
                       grow_script=es_files[constants.ES_SCRIPT_GROW],
1690
                       attach_script=es_files[constants.ES_SCRIPT_ATTACH],
1691
                       detach_script=es_files[constants.ES_SCRIPT_DETACH],
1692
                       setinfo_script=es_files[constants.ES_SCRIPT_SETINFO],
1693
                       verify_script=es_files[constants.ES_SCRIPT_VERIFY],
1694
                       supported_parameters=parameters)
1695
  return True, es_obj
1696

    
1697

    
1698
def _ExtStorageEnvironment(unique_id, ext_params,
1699
                           size=None, grow=None, metadata=None):
1700
  """Calculate the environment for an External Storage script.
1701

1702
  @type unique_id: tuple (driver, vol_name)
1703
  @param unique_id: ExtStorage pool and name of the Volume
1704
  @type ext_params: dict
1705
  @param ext_params: the EXT parameters
1706
  @type size: string
1707
  @param size: size of the Volume (in mebibytes)
1708
  @type grow: string
1709
  @param grow: new size of Volume after grow (in mebibytes)
1710
  @type metadata: string
1711
  @param metadata: metadata info of the Volume
1712
  @rtype: dict
1713
  @return: dict of environment variables
1714

1715
  """
1716
  vol_name = unique_id[1]
1717

    
1718
  result = {}
1719
  result["VOL_NAME"] = vol_name
1720

    
1721
  # EXT params
1722
  for pname, pvalue in ext_params.items():
1723
    result["EXTP_%s" % pname.upper()] = str(pvalue)
1724

    
1725
  if size is not None:
1726
    result["VOL_SIZE"] = size
1727

    
1728
  if grow is not None:
1729
    result["VOL_NEW_SIZE"] = grow
1730

    
1731
  if metadata is not None:
1732
    result["VOL_METADATA"] = metadata
1733

    
1734
  return result
1735

    
1736

    
1737
def _VolumeLogName(kind, es_name, volume):
1738
  """Compute the ExtStorage log filename for a given Volume and operation.
1739

1740
  @type kind: string
1741
  @param kind: the operation type (e.g. create, remove etc.)
1742
  @type es_name: string
1743
  @param es_name: the ExtStorage name
1744
  @type volume: string
1745
  @param volume: the name of the Volume inside the External Storage
1746

1747
  """
1748
  # Check if the extstorage log dir is a valid dir
1749
  if not os.path.isdir(pathutils.LOG_ES_DIR):
1750
    base.ThrowError("Cannot find log directory: %s", pathutils.LOG_ES_DIR)
1751

    
1752
  # TODO: Use tempfile.mkstemp to create unique filename
1753
  basename = ("%s-%s-%s-%s.log" %
1754
              (kind, es_name, volume, utils.TimestampForFilename()))
1755
  return utils.PathJoin(pathutils.LOG_ES_DIR, basename)
1756

    
1757

    
1758
DEV_MAP = {
1759
  constants.LD_LV: LogicalVolume,
1760
  constants.LD_DRBD8: drbd.DRBD8Dev,
1761
  constants.LD_BLOCKDEV: PersistentBlockDevice,
1762
  constants.LD_RBD: RADOSBlockDevice,
1763
  constants.LD_EXT: ExtStorageDevice,
1764
  }
1765

    
1766
if constants.ENABLE_FILE_STORAGE or constants.ENABLE_SHARED_FILE_STORAGE:
1767
  DEV_MAP[constants.LD_FILE] = FileStorage
1768

    
1769

    
1770
def _VerifyDiskType(dev_type):
1771
  if dev_type not in DEV_MAP:
1772
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1773

    
1774

    
1775
def _VerifyDiskParams(disk):
1776
  """Verifies if all disk parameters are set.
1777

1778
  """
1779
  missing = set(constants.DISK_LD_DEFAULTS[disk.dev_type]) - set(disk.params)
1780
  if missing:
1781
    raise errors.ProgrammerError("Block device is missing disk parameters: %s" %
1782
                                 missing)
1783

    
1784

    
1785
def FindDevice(disk, children):
1786
  """Search for an existing, assembled device.
1787

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

1791
  @type disk: L{objects.Disk}
1792
  @param disk: the disk object to find
1793
  @type children: list of L{bdev.BlockDev}
1794
  @param children: the list of block devices that are children of the device
1795
                  represented by the disk parameter
1796

1797
  """
1798
  _VerifyDiskType(disk.dev_type)
1799
  device = DEV_MAP[disk.dev_type](disk.physical_id, children, disk.size,
1800
                                  disk.params)
1801
  if not device.attached:
1802
    return None
1803
  return device
1804

    
1805

    
1806
def Assemble(disk, children):
1807
  """Try to attach or assemble an existing device.
1808

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

1812
  @type disk: L{objects.Disk}
1813
  @param disk: the disk object to assemble
1814
  @type children: list of L{bdev.BlockDev}
1815
  @param children: the list of block devices that are children of the device
1816
                  represented by the disk parameter
1817

1818
  """
1819
  _VerifyDiskType(disk.dev_type)
1820
  _VerifyDiskParams(disk)
1821
  device = DEV_MAP[disk.dev_type](disk.physical_id, children, disk.size,
1822
                                  disk.params)
1823
  device.Assemble()
1824
  return device
1825

    
1826

    
1827
def Create(disk, children, excl_stor):
1828
  """Create a device.
1829

1830
  @type disk: L{objects.Disk}
1831
  @param disk: the disk object to create
1832
  @type children: list of L{bdev.BlockDev}
1833
  @param children: the list of block devices that are children of the device
1834
                  represented by the disk parameter
1835
  @type excl_stor: boolean
1836
  @param excl_stor: Whether exclusive_storage is active
1837
  @rtype: L{bdev.BlockDev}
1838
  @return: the created device, or C{None} in case of an error
1839

1840
  """
1841
  _VerifyDiskType(disk.dev_type)
1842
  _VerifyDiskParams(disk)
1843
  device = DEV_MAP[disk.dev_type].Create(disk.physical_id, children, disk.size,
1844
                                         disk.spindles, disk.params, excl_stor)
1845
  return device