Statistics
| Branch: | Tag: | Revision:

root / lib / storage / bdev.py @ be9150ea

History | View | Annotate | Download (58.5 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
      if spindles is None:
266
        base.ThrowError("Unspecified number of spindles: this is required"
267
                        "when exclusive storage is enabled, try running"
268
                        " gnt-cluster repair-disk-sizes")
269
      (err_msgs, _) = utils.LvmExclusiveCheckNodePvs(pvs_info)
270
      if err_msgs:
271
        for m in err_msgs:
272
          logging.warning(m)
273
      req_pvs = cls._ComputeNumPvs(size, pvs_info)
274
      if spindles < req_pvs:
275
        base.ThrowError("Requested number of spindles (%s) is not enough for"
276
                        " a disk of %d MB (at least %d spindles needed)",
277
                        spindles, size, req_pvs)
278
      else:
279
        req_pvs = spindles
280
      pvlist = cls._GetEmptyPvNames(pvs_info, req_pvs)
281
      current_pvs = len(pvlist)
282
      if current_pvs < req_pvs:
283
        base.ThrowError("Not enough empty PVs (spindles) to create a disk of %d"
284
                        " MB: %d available, %d needed",
285
                        size, current_pvs, req_pvs)
286
      assert current_pvs == len(pvlist)
287
      # We must update stripes to be sure to use all the desired spindles
288
      stripes = current_pvs
289
      if stripes > desired_stripes:
290
        # Don't warn when lowering stripes, as it's no surprise
291
        logging.warning("Using %s stripes instead of %s, to be able to use"
292
                        " %s spindles", stripes, desired_stripes, current_pvs)
293

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

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

    
319
  @staticmethod
320
  def _GetVolumeInfo(lvm_cmd, fields):
321
    """Returns LVM Volume infos using lvm_cmd
322

323
    @param lvm_cmd: Should be one of "pvs", "vgs" or "lvs"
324
    @param fields: Fields to return
325
    @return: A list of dicts each with the parsed fields
326

327
    """
328
    if not fields:
329
      raise errors.ProgrammerError("No fields specified")
330

    
331
    sep = "|"
332
    cmd = [lvm_cmd, "--noheadings", "--nosuffix", "--units=m", "--unbuffered",
333
           "--separator=%s" % sep, "-o%s" % ",".join(fields)]
334

    
335
    result = utils.RunCmd(cmd)
336
    if result.failed:
337
      raise errors.CommandError("Can't get the volume information: %s - %s" %
338
                                (result.fail_reason, result.output))
339

    
340
    data = []
341
    for line in result.stdout.splitlines():
342
      splitted_fields = line.strip().split(sep)
343

    
344
      if len(fields) != len(splitted_fields):
345
        raise errors.CommandError("Can't parse %s output: line '%s'" %
346
                                  (lvm_cmd, line))
347

    
348
      data.append(splitted_fields)
349

    
350
    return data
351

    
352
  @classmethod
353
  def GetPVInfo(cls, vg_names, filter_allocatable=True, include_lvs=False):
354
    """Get the free space info for PVs in a volume group.
355

356
    @param vg_names: list of volume group names, if empty all will be returned
357
    @param filter_allocatable: whether to skip over unallocatable PVs
358
    @param include_lvs: whether to include a list of LVs hosted on each PV
359

360
    @rtype: list
361
    @return: list of objects.LvmPvInfo objects
362

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

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

    
407
    return data
408

    
409
  @classmethod
410
  def _GetRawFreePvInfo(cls, vg_name):
411
    """Return info (size/free) about PVs.
412

413
    @type vg_name: string
414
    @param vg_name: VG name
415
    @rtype: tuple
416
    @return: (standard_pv_size_in_MiB, number_of_free_pvs, total_number_of_pvs)
417

418
    """
419
    pvs_info = cls.GetPVInfo([vg_name])
420
    if not pvs_info:
421
      pv_size = 0.0
422
      free_pvs = 0
423
      num_pvs = 0
424
    else:
425
      pv_size = cls._GetStdPvSize(pvs_info)
426
      free_pvs = len(cls._GetEmptyPvNames(pvs_info))
427
      num_pvs = len(pvs_info)
428
    return (pv_size, free_pvs, num_pvs)
429

    
430
  @classmethod
431
  def _GetExclusiveStorageVgFree(cls, vg_name):
432
    """Return the free disk space in the given VG, in exclusive storage mode.
433

434
    @type vg_name: string
435
    @param vg_name: VG name
436
    @rtype: float
437
    @return: free space in MiB
438
    """
439
    (pv_size, free_pvs, _) = cls._GetRawFreePvInfo(vg_name)
440
    return pv_size * free_pvs
441

    
442
  @classmethod
443
  def GetVgSpindlesInfo(cls, vg_name):
444
    """Get the free space info for specific VGs.
445

446
    @param vg_name: volume group name
447
    @rtype: tuple
448
    @return: (free_spindles, total_spindles)
449

450
    """
451
    (_, free_pvs, num_pvs) = cls._GetRawFreePvInfo(vg_name)
452
    return (free_pvs, num_pvs)
453

    
454
  @classmethod
455
  def GetVGInfo(cls, vg_names, excl_stor, filter_readonly=True):
456
    """Get the free space info for specific VGs.
457

458
    @param vg_names: list of volume group names, if empty all will be returned
459
    @param excl_stor: whether exclusive_storage is enabled
460
    @param filter_readonly: whether to skip over readonly VGs
461

462
    @rtype: list
463
    @return: list of tuples (free_space, total_size, name) with free_space in
464
             MiB
465

466
    """
467
    try:
468
      info = cls._GetVolumeInfo("vgs", ["vg_name", "vg_free", "vg_attr",
469
                                        "vg_size"])
470
    except errors.GenericError, err:
471
      logging.error("Can't get VG information: %s", err)
472
      return None
473

    
474
    data = []
475
    for vg_name, vg_free, vg_attr, vg_size in info:
476
      # (possibly) skip over vgs which are not writable
477
      if filter_readonly and vg_attr[0] == "r":
478
        continue
479
      # (possibly) skip over vgs which are not in the right volume group(s)
480
      if vg_names and vg_name not in vg_names:
481
        continue
482
      # Exclusive storage needs a different concept of free space
483
      if excl_stor:
484
        es_free = cls._GetExclusiveStorageVgFree(vg_name)
485
        assert es_free <= vg_free
486
        vg_free = es_free
487
      data.append((float(vg_free), float(vg_size), vg_name))
488

    
489
    return data
490

    
491
  @classmethod
492
  def _ValidateName(cls, name):
493
    """Validates that a given name is valid as VG or LV name.
494

495
    The list of valid characters and restricted names is taken out of
496
    the lvm(8) manpage, with the simplification that we enforce both
497
    VG and LV restrictions on the names.
498

499
    """
500
    if (not cls._VALID_NAME_RE.match(name) or
501
        name in cls._INVALID_NAMES or
502
        compat.any(substring in name for substring in cls._INVALID_SUBSTRINGS)):
503
      base.ThrowError("Invalid LVM name '%s'", name)
504

    
505
  def Remove(self):
506
    """Remove this logical volume.
507

508
    """
509
    if not self.minor and not self.Attach():
510
      # the LV does not exist
511
      return
512
    result = utils.RunCmd(["lvremove", "-f", "%s/%s" %
513
                           (self._vg_name, self._lv_name)])
514
    if result.failed:
515
      base.ThrowError("Can't lvremove: %s - %s",
516
                      result.fail_reason, result.output)
517

    
518
  def Rename(self, new_id):
519
    """Rename this logical volume.
520

521
    """
522
    if not isinstance(new_id, (tuple, list)) or len(new_id) != 2:
523
      raise errors.ProgrammerError("Invalid new logical id '%s'" % new_id)
524
    new_vg, new_name = new_id
525
    if new_vg != self._vg_name:
526
      raise errors.ProgrammerError("Can't move a logical volume across"
527
                                   " volume groups (from %s to to %s)" %
528
                                   (self._vg_name, new_vg))
529
    result = utils.RunCmd(["lvrename", new_vg, self._lv_name, new_name])
530
    if result.failed:
531
      base.ThrowError("Failed to rename the logical volume: %s", result.output)
532
    self._lv_name = new_name
533
    self.dev_path = utils.PathJoin("/dev", self._vg_name, self._lv_name)
534

    
535
  @classmethod
536
  def _ParseLvInfoLine(cls, line, sep):
537
    """Parse one line of the lvs output used in L{_GetLvInfo}.
538

539
    """
540
    elems = line.strip().rstrip(sep).split(sep)
541
    if len(elems) != 6:
542
      base.ThrowError("Can't parse LVS output, len(%s) != 6", str(elems))
543

    
544
    (status, major, minor, pe_size, stripes, pvs) = elems
545
    if len(status) < 6:
546
      base.ThrowError("lvs lv_attr is not at least 6 characters (%s)", status)
547

    
548
    try:
549
      major = int(major)
550
      minor = int(minor)
551
    except (TypeError, ValueError), err:
552
      base.ThrowError("lvs major/minor cannot be parsed: %s", str(err))
553

    
554
    try:
555
      pe_size = int(float(pe_size))
556
    except (TypeError, ValueError), err:
557
      base.ThrowError("Can't parse vg extent size: %s", err)
558

    
559
    try:
560
      stripes = int(stripes)
561
    except (TypeError, ValueError), err:
562
      base.ThrowError("Can't parse the number of stripes: %s", err)
563

    
564
    pv_names = []
565
    for pv in pvs.split(","):
566
      m = re.match(cls._PARSE_PV_DEV_RE, pv)
567
      if not m:
568
        base.ThrowError("Can't parse this device list: %s", pvs)
569
      pv_names.append(m.group(1))
570
    assert len(pv_names) > 0
571

    
572
    return (status, major, minor, pe_size, stripes, pv_names)
573

    
574
  @classmethod
575
  def _GetLvInfo(cls, dev_path, _run_cmd=utils.RunCmd):
576
    """Get info about the given existing LV to be used.
577

578
    """
579
    sep = "|"
580
    result = _run_cmd(["lvs", "--noheadings", "--separator=%s" % sep,
581
                       "--units=k", "--nosuffix",
582
                       "-olv_attr,lv_kernel_major,lv_kernel_minor,"
583
                       "vg_extent_size,stripes,devices", dev_path])
584
    if result.failed:
585
      base.ThrowError("Can't find LV %s: %s, %s",
586
                      dev_path, result.fail_reason, result.output)
587
    # the output can (and will) have multiple lines for multi-segment
588
    # LVs, as the 'stripes' parameter is a segment one, so we take
589
    # only the last entry, which is the one we're interested in; note
590
    # that with LVM2 anyway the 'stripes' value must be constant
591
    # across segments, so this is a no-op actually
592
    out = result.stdout.splitlines()
593
    if not out: # totally empty result? splitlines() returns at least
594
                # one line for any non-empty string
595
      base.ThrowError("Can't parse LVS output, no lines? Got '%s'", str(out))
596
    pv_names = set()
597
    for line in out:
598
      (status, major, minor, pe_size, stripes, more_pvs) = \
599
        cls._ParseLvInfoLine(line, sep)
600
      pv_names.update(more_pvs)
601
    return (status, major, minor, pe_size, stripes, pv_names)
602

    
603
  def Attach(self):
604
    """Attach to an existing LV.
605

606
    This method will try to see if an existing and active LV exists
607
    which matches our name. If so, its major/minor will be
608
    recorded.
609

610
    """
611
    self.attached = False
612
    try:
613
      (status, major, minor, pe_size, stripes, pv_names) = \
614
        self._GetLvInfo(self.dev_path)
615
    except errors.BlockDeviceError:
616
      return False
617

    
618
    self.major = major
619
    self.minor = minor
620
    self.pe_size = pe_size
621
    self.stripe_count = stripes
622
    self._degraded = status[0] == "v" # virtual volume, i.e. doesn't backing
623
                                      # storage
624
    self.pv_names = pv_names
625
    self.attached = True
626
    return True
627

    
628
  def Assemble(self):
629
    """Assemble the device.
630

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

635
    """
636
    result = utils.RunCmd(["lvchange", "-ay", self.dev_path])
637
    if result.failed:
638
      base.ThrowError("Can't activate lv %s: %s", self.dev_path, result.output)
639

    
640
  def Shutdown(self):
641
    """Shutdown the device.
642

643
    This is a no-op for the LV device type, as we don't deactivate the
644
    volumes on shutdown.
645

646
    """
647
    pass
648

    
649
  def GetSyncStatus(self):
650
    """Returns the sync status of the device.
651

652
    If this device is a mirroring device, this function returns the
653
    status of the mirror.
654

655
    For logical volumes, sync_percent and estimated_time are always
656
    None (no recovery in progress, as we don't handle the mirrored LV
657
    case). The is_degraded parameter is the inverse of the ldisk
658
    parameter.
659

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

666
    The status was already read in Attach, so we just return it.
667

668
    @rtype: objects.BlockDevStatus
669

670
    """
671
    if self._degraded:
672
      ldisk_status = constants.LDS_FAULTY
673
    else:
674
      ldisk_status = constants.LDS_OKAY
675

    
676
    return objects.BlockDevStatus(dev_path=self.dev_path,
677
                                  major=self.major,
678
                                  minor=self.minor,
679
                                  sync_percent=None,
680
                                  estimated_time=None,
681
                                  is_degraded=self._degraded,
682
                                  ldisk_status=ldisk_status)
683

    
684
  def Open(self, force=False):
685
    """Make the device ready for I/O.
686

687
    This is a no-op for the LV device type.
688

689
    """
690
    pass
691

    
692
  def Close(self):
693
    """Notifies that the device will no longer be used for I/O.
694

695
    This is a no-op for the LV device type.
696

697
    """
698
    pass
699

    
700
  def Snapshot(self, size):
701
    """Create a snapshot copy of an lvm block device.
702

703
    @returns: tuple (vg, lv)
704

705
    """
706
    snap_name = self._lv_name + ".snap"
707

    
708
    # remove existing snapshot if found
709
    snap = LogicalVolume((self._vg_name, snap_name), None, size, self.params)
710
    base.IgnoreError(snap.Remove)
711

    
712
    vg_info = self.GetVGInfo([self._vg_name], False)
713
    if not vg_info:
714
      base.ThrowError("Can't compute VG info for vg %s", self._vg_name)
715
    free_size, _, _ = vg_info[0]
716
    if free_size < size:
717
      base.ThrowError("Not enough free space: required %s,"
718
                      " available %s", size, free_size)
719

    
720
    _CheckResult(utils.RunCmd(["lvcreate", "-L%dm" % size, "-s",
721
                               "-n%s" % snap_name, self.dev_path]))
722

    
723
    return (self._vg_name, snap_name)
724

    
725
  def _RemoveOldInfo(self):
726
    """Try to remove old tags from the lv.
727

728
    """
729
    result = utils.RunCmd(["lvs", "-o", "tags", "--noheadings", "--nosuffix",
730
                           self.dev_path])
731
    _CheckResult(result)
732

    
733
    raw_tags = result.stdout.strip()
734
    if raw_tags:
735
      for tag in raw_tags.split(","):
736
        _CheckResult(utils.RunCmd(["lvchange", "--deltag",
737
                                   tag.strip(), self.dev_path]))
738

    
739
  def SetInfo(self, text):
740
    """Update metadata with info text.
741

742
    """
743
    base.BlockDev.SetInfo(self, text)
744

    
745
    self._RemoveOldInfo()
746

    
747
    # Replace invalid characters
748
    text = re.sub("^[^A-Za-z0-9_+.]", "_", text)
749
    text = re.sub("[^-A-Za-z0-9_+.]", "_", text)
750

    
751
    # Only up to 128 characters are allowed
752
    text = text[:128]
753

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

    
756
  def Grow(self, amount, dryrun, backingstore, excl_stor):
757
    """Grow the logical volume.
758

759
    """
760
    if not backingstore:
761
      return
762
    if self.pe_size is None or self.stripe_count is None:
763
      if not self.Attach():
764
        base.ThrowError("Can't attach to LV during Grow()")
765
    full_stripe_size = self.pe_size * self.stripe_count
766
    # pe_size is in KB
767
    amount *= 1024
768
    rest = amount % full_stripe_size
769
    if rest != 0:
770
      amount += full_stripe_size - rest
771
    cmd = ["lvextend", "-L", "+%dk" % amount]
772
    if dryrun:
773
      cmd.append("--test")
774
    if excl_stor:
775
      # Disk growth doesn't grow the number of spindles, so we must stay within
776
      # our assigned volumes
777
      pvlist = list(self.pv_names)
778
    else:
779
      pvlist = []
780
    # we try multiple algorithms since the 'best' ones might not have
781
    # space available in the right place, but later ones might (since
782
    # they have less constraints); also note that only recent LVM
783
    # supports 'cling'
784
    for alloc_policy in "contiguous", "cling", "normal":
785
      result = utils.RunCmd(cmd + ["--alloc", alloc_policy, self.dev_path] +
786
                            pvlist)
787
      if not result.failed:
788
        return
789
    base.ThrowError("Can't grow LV %s: %s", self.dev_path, result.output)
790

    
791
  def GetActualSpindles(self):
792
    """Return the number of spindles used.
793

794
    """
795
    assert self.attached, "BlockDevice not attached in GetActualSpindles()"
796
    return len(self.pv_names)
797

    
798

    
799
class FileStorage(base.BlockDev):
800
  """File device.
801

802
  This class represents the a file storage backend device.
803

804
  The unique_id for the file device is a (file_driver, file_path) tuple.
805

806
  """
807
  def __init__(self, unique_id, children, size, params):
808
    """Initalizes a file device backend.
809

810
    """
811
    if children:
812
      raise errors.BlockDeviceError("Invalid setup for file device")
813
    super(FileStorage, self).__init__(unique_id, children, size, params)
814
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
815
      raise ValueError("Invalid configuration data %s" % str(unique_id))
816
    self.driver = unique_id[0]
817
    self.dev_path = unique_id[1]
818

    
819
    CheckFileStoragePath(self.dev_path)
820

    
821
    self.Attach()
822

    
823
  def Assemble(self):
824
    """Assemble the device.
825

826
    Checks whether the file device exists, raises BlockDeviceError otherwise.
827

828
    """
829
    if not os.path.exists(self.dev_path):
830
      base.ThrowError("File device '%s' does not exist" % self.dev_path)
831

    
832
  def Shutdown(self):
833
    """Shutdown the device.
834

835
    This is a no-op for the file type, as we don't deactivate
836
    the file on shutdown.
837

838
    """
839
    pass
840

    
841
  def Open(self, force=False):
842
    """Make the device ready for I/O.
843

844
    This is a no-op for the file type.
845

846
    """
847
    pass
848

    
849
  def Close(self):
850
    """Notifies that the device will no longer be used for I/O.
851

852
    This is a no-op for the file type.
853

854
    """
855
    pass
856

    
857
  def Remove(self):
858
    """Remove the file backing the block device.
859

860
    @rtype: boolean
861
    @return: True if the removal was successful
862

863
    """
864
    try:
865
      os.remove(self.dev_path)
866
    except OSError, err:
867
      if err.errno != errno.ENOENT:
868
        base.ThrowError("Can't remove file '%s': %s", self.dev_path, err)
869

    
870
  def Rename(self, new_id):
871
    """Renames the file.
872

873
    """
874
    # TODO: implement rename for file-based storage
875
    base.ThrowError("Rename is not supported for file-based storage")
876

    
877
  def Grow(self, amount, dryrun, backingstore, excl_stor):
878
    """Grow the file
879

880
    @param amount: the amount (in mebibytes) to grow with
881

882
    """
883
    if not backingstore:
884
      return
885
    # Check that the file exists
886
    self.Assemble()
887
    current_size = self.GetActualSize()
888
    new_size = current_size + amount * 1024 * 1024
889
    assert new_size > current_size, "Cannot Grow with a negative amount"
890
    # We can't really simulate the growth
891
    if dryrun:
892
      return
893
    try:
894
      f = open(self.dev_path, "a+")
895
      f.truncate(new_size)
896
      f.close()
897
    except EnvironmentError, err:
898
      base.ThrowError("Error in file growth: %", str(err))
899

    
900
  def Attach(self):
901
    """Attach to an existing file.
902

903
    Check if this file already exists.
904

905
    @rtype: boolean
906
    @return: True if file exists
907

908
    """
909
    self.attached = os.path.exists(self.dev_path)
910
    return self.attached
911

    
912
  def GetActualSize(self):
913
    """Return the actual disk size.
914

915
    @note: the device needs to be active when this is called
916

917
    """
918
    assert self.attached, "BlockDevice not attached in GetActualSize()"
919
    try:
920
      st = os.stat(self.dev_path)
921
      return st.st_size
922
    except OSError, err:
923
      base.ThrowError("Can't stat %s: %s", self.dev_path, err)
924

    
925
  @classmethod
926
  def Create(cls, unique_id, children, size, spindles, params, excl_stor):
927
    """Create a new file.
928

929
    @param size: the size of file in MiB
930

931
    @rtype: L{bdev.FileStorage}
932
    @return: an instance of FileStorage
933

934
    """
935
    if excl_stor:
936
      raise errors.ProgrammerError("FileStorage device requested with"
937
                                   " exclusive_storage")
938
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
939
      raise ValueError("Invalid configuration data %s" % str(unique_id))
940

    
941
    dev_path = unique_id[1]
942

    
943
    CheckFileStoragePath(dev_path)
944

    
945
    try:
946
      fd = os.open(dev_path, os.O_RDWR | os.O_CREAT | os.O_EXCL)
947
      f = os.fdopen(fd, "w")
948
      f.truncate(size * 1024 * 1024)
949
      f.close()
950
    except EnvironmentError, err:
951
      if err.errno == errno.EEXIST:
952
        base.ThrowError("File already existing: %s", dev_path)
953
      base.ThrowError("Error in file creation: %", str(err))
954

    
955
    return FileStorage(unique_id, children, size, params)
956

    
957

    
958
class PersistentBlockDevice(base.BlockDev):
959
  """A block device with persistent node
960

961
  May be either directly attached, or exposed through DM (e.g. dm-multipath).
962
  udev helpers are probably required to give persistent, human-friendly
963
  names.
964

965
  For the time being, pathnames are required to lie under /dev.
966

967
  """
968
  def __init__(self, unique_id, children, size, params):
969
    """Attaches to a static block device.
970

971
    The unique_id is a path under /dev.
972

973
    """
974
    super(PersistentBlockDevice, self).__init__(unique_id, children, size,
975
                                                params)
976
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
977
      raise ValueError("Invalid configuration data %s" % str(unique_id))
978
    self.dev_path = unique_id[1]
979
    if not os.path.realpath(self.dev_path).startswith("/dev/"):
980
      raise ValueError("Full path '%s' lies outside /dev" %
981
                              os.path.realpath(self.dev_path))
982
    # TODO: this is just a safety guard checking that we only deal with devices
983
    # we know how to handle. In the future this will be integrated with
984
    # external storage backends and possible values will probably be collected
985
    # from the cluster configuration.
986
    if unique_id[0] != constants.BLOCKDEV_DRIVER_MANUAL:
987
      raise ValueError("Got persistent block device of invalid type: %s" %
988
                       unique_id[0])
989

    
990
    self.major = self.minor = None
991
    self.Attach()
992

    
993
  @classmethod
994
  def Create(cls, unique_id, children, size, spindles, params, excl_stor):
995
    """Create a new device
996

997
    This is a noop, we only return a PersistentBlockDevice instance
998

999
    """
1000
    if excl_stor:
1001
      raise errors.ProgrammerError("Persistent block device requested with"
1002
                                   " exclusive_storage")
1003
    return PersistentBlockDevice(unique_id, children, 0, params)
1004

    
1005
  def Remove(self):
1006
    """Remove a device
1007

1008
    This is a noop
1009

1010
    """
1011
    pass
1012

    
1013
  def Rename(self, new_id):
1014
    """Rename this device.
1015

1016
    """
1017
    base.ThrowError("Rename is not supported for PersistentBlockDev storage")
1018

    
1019
  def Attach(self):
1020
    """Attach to an existing block device.
1021

1022

1023
    """
1024
    self.attached = False
1025
    try:
1026
      st = os.stat(self.dev_path)
1027
    except OSError, err:
1028
      logging.error("Error stat()'ing %s: %s", self.dev_path, str(err))
1029
      return False
1030

    
1031
    if not stat.S_ISBLK(st.st_mode):
1032
      logging.error("%s is not a block device", self.dev_path)
1033
      return False
1034

    
1035
    self.major = os.major(st.st_rdev)
1036
    self.minor = os.minor(st.st_rdev)
1037
    self.attached = True
1038

    
1039
    return True
1040

    
1041
  def Assemble(self):
1042
    """Assemble the device.
1043

1044
    """
1045
    pass
1046

    
1047
  def Shutdown(self):
1048
    """Shutdown the device.
1049

1050
    """
1051
    pass
1052

    
1053
  def Open(self, force=False):
1054
    """Make the device ready for I/O.
1055

1056
    """
1057
    pass
1058

    
1059
  def Close(self):
1060
    """Notifies that the device will no longer be used for I/O.
1061

1062
    """
1063
    pass
1064

    
1065
  def Grow(self, amount, dryrun, backingstore, excl_stor):
1066
    """Grow the logical volume.
1067

1068
    """
1069
    base.ThrowError("Grow is not supported for PersistentBlockDev storage")
1070

    
1071

    
1072
class RADOSBlockDevice(base.BlockDev):
1073
  """A RADOS Block Device (rbd).
1074

1075
  This class implements the RADOS Block Device for the backend. You need
1076
  the rbd kernel driver, the RADOS Tools and a working RADOS cluster for
1077
  this to be functional.
1078

1079
  """
1080
  def __init__(self, unique_id, children, size, params):
1081
    """Attaches to an rbd device.
1082

1083
    """
1084
    super(RADOSBlockDevice, self).__init__(unique_id, children, size, params)
1085
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1086
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1087

    
1088
    self.driver, self.rbd_name = unique_id
1089

    
1090
    self.major = self.minor = None
1091
    self.Attach()
1092

    
1093
  @classmethod
1094
  def Create(cls, unique_id, children, size, spindles, params, excl_stor):
1095
    """Create a new rbd device.
1096

1097
    Provision a new rbd volume inside a RADOS pool.
1098

1099
    """
1100
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1101
      raise errors.ProgrammerError("Invalid configuration data %s" %
1102
                                   str(unique_id))
1103
    if excl_stor:
1104
      raise errors.ProgrammerError("RBD device requested with"
1105
                                   " exclusive_storage")
1106
    rbd_pool = params[constants.LDP_POOL]
1107
    rbd_name = unique_id[1]
1108

    
1109
    # Provision a new rbd volume (Image) inside the RADOS cluster.
1110
    cmd = [constants.RBD_CMD, "create", "-p", rbd_pool,
1111
           rbd_name, "--size", "%s" % size]
1112
    result = utils.RunCmd(cmd)
1113
    if result.failed:
1114
      base.ThrowError("rbd creation failed (%s): %s",
1115
                      result.fail_reason, result.output)
1116

    
1117
    return RADOSBlockDevice(unique_id, children, size, params)
1118

    
1119
  def Remove(self):
1120
    """Remove the rbd device.
1121

1122
    """
1123
    rbd_pool = self.params[constants.LDP_POOL]
1124
    rbd_name = self.unique_id[1]
1125

    
1126
    if not self.minor and not self.Attach():
1127
      # The rbd device doesn't exist.
1128
      return
1129

    
1130
    # First shutdown the device (remove mappings).
1131
    self.Shutdown()
1132

    
1133
    # Remove the actual Volume (Image) from the RADOS cluster.
1134
    cmd = [constants.RBD_CMD, "rm", "-p", rbd_pool, rbd_name]
1135
    result = utils.RunCmd(cmd)
1136
    if result.failed:
1137
      base.ThrowError("Can't remove Volume from cluster with rbd rm: %s - %s",
1138
                      result.fail_reason, result.output)
1139

    
1140
  def Rename(self, new_id):
1141
    """Rename this device.
1142

1143
    """
1144
    pass
1145

    
1146
  def Attach(self):
1147
    """Attach to an existing rbd device.
1148

1149
    This method maps the rbd volume that matches our name with
1150
    an rbd device and then attaches to this device.
1151

1152
    """
1153
    self.attached = False
1154

    
1155
    # Map the rbd volume to a block device under /dev
1156
    self.dev_path = self._MapVolumeToBlockdev(self.unique_id)
1157

    
1158
    try:
1159
      st = os.stat(self.dev_path)
1160
    except OSError, err:
1161
      logging.error("Error stat()'ing %s: %s", self.dev_path, str(err))
1162
      return False
1163

    
1164
    if not stat.S_ISBLK(st.st_mode):
1165
      logging.error("%s is not a block device", self.dev_path)
1166
      return False
1167

    
1168
    self.major = os.major(st.st_rdev)
1169
    self.minor = os.minor(st.st_rdev)
1170
    self.attached = True
1171

    
1172
    return True
1173

    
1174
  def _MapVolumeToBlockdev(self, unique_id):
1175
    """Maps existing rbd volumes to block devices.
1176

1177
    This method should be idempotent if the mapping already exists.
1178

1179
    @rtype: string
1180
    @return: the block device path that corresponds to the volume
1181

1182
    """
1183
    pool = self.params[constants.LDP_POOL]
1184
    name = unique_id[1]
1185

    
1186
    # Check if the mapping already exists.
1187
    rbd_dev = self._VolumeToBlockdev(pool, name)
1188
    if rbd_dev:
1189
      # The mapping exists. Return it.
1190
      return rbd_dev
1191

    
1192
    # The mapping doesn't exist. Create it.
1193
    map_cmd = [constants.RBD_CMD, "map", "-p", pool, name]
1194
    result = utils.RunCmd(map_cmd)
1195
    if result.failed:
1196
      base.ThrowError("rbd map failed (%s): %s",
1197
                      result.fail_reason, result.output)
1198

    
1199
    # Find the corresponding rbd device.
1200
    rbd_dev = self._VolumeToBlockdev(pool, name)
1201
    if not rbd_dev:
1202
      base.ThrowError("rbd map succeeded, but could not find the rbd block"
1203
                      " device in output of showmapped, for volume: %s", name)
1204

    
1205
    # The device was successfully mapped. Return it.
1206
    return rbd_dev
1207

    
1208
  @classmethod
1209
  def _VolumeToBlockdev(cls, pool, volume_name):
1210
    """Do the 'volume name'-to-'rbd block device' resolving.
1211

1212
    @type pool: string
1213
    @param pool: RADOS pool to use
1214
    @type volume_name: string
1215
    @param volume_name: the name of the volume whose device we search for
1216
    @rtype: string or None
1217
    @return: block device path if the volume is mapped, else None
1218

1219
    """
1220
    try:
1221
      # Newer versions of the rbd tool support json output formatting. Use it
1222
      # if available.
1223
      showmap_cmd = [
1224
        constants.RBD_CMD,
1225
        "showmapped",
1226
        "-p",
1227
        pool,
1228
        "--format",
1229
        "json"
1230
        ]
1231
      result = utils.RunCmd(showmap_cmd)
1232
      if result.failed:
1233
        logging.error("rbd JSON output formatting returned error (%s): %s,"
1234
                      "falling back to plain output parsing",
1235
                      result.fail_reason, result.output)
1236
        raise RbdShowmappedJsonError
1237

    
1238
      return cls._ParseRbdShowmappedJson(result.output, volume_name)
1239
    except RbdShowmappedJsonError:
1240
      # For older versions of rbd, we have to parse the plain / text output
1241
      # manually.
1242
      showmap_cmd = [constants.RBD_CMD, "showmapped", "-p", pool]
1243
      result = utils.RunCmd(showmap_cmd)
1244
      if result.failed:
1245
        base.ThrowError("rbd showmapped failed (%s): %s",
1246
                        result.fail_reason, result.output)
1247

    
1248
      return cls._ParseRbdShowmappedPlain(result.output, volume_name)
1249

    
1250
  @staticmethod
1251
  def _ParseRbdShowmappedJson(output, volume_name):
1252
    """Parse the json output of `rbd showmapped'.
1253

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

1257
    @type output: string
1258
    @param output: the json output of `rbd showmapped'
1259
    @type volume_name: string
1260
    @param volume_name: the name of the volume whose device we search for
1261
    @rtype: string or None
1262
    @return: block device path if the volume is mapped, else None
1263

1264
    """
1265
    try:
1266
      devices = serializer.LoadJson(output)
1267
    except ValueError, err:
1268
      base.ThrowError("Unable to parse JSON data: %s" % err)
1269

    
1270
    rbd_dev = None
1271
    for d in devices.values(): # pylint: disable=E1103
1272
      try:
1273
        name = d["name"]
1274
      except KeyError:
1275
        base.ThrowError("'name' key missing from json object %s", devices)
1276

    
1277
      if name == volume_name:
1278
        if rbd_dev is not None:
1279
          base.ThrowError("rbd volume %s is mapped more than once", volume_name)
1280

    
1281
        rbd_dev = d["device"]
1282

    
1283
    return rbd_dev
1284

    
1285
  @staticmethod
1286
  def _ParseRbdShowmappedPlain(output, volume_name):
1287
    """Parse the (plain / text) output of `rbd showmapped'.
1288

1289
    This method parses the output of `rbd showmapped' and returns
1290
    the rbd block device path (e.g. /dev/rbd0) that matches the
1291
    given rbd volume.
1292

1293
    @type output: string
1294
    @param output: the plain text output of `rbd showmapped'
1295
    @type volume_name: string
1296
    @param volume_name: the name of the volume whose device we search for
1297
    @rtype: string or None
1298
    @return: block device path if the volume is mapped, else None
1299

1300
    """
1301
    allfields = 5
1302
    volumefield = 2
1303
    devicefield = 4
1304

    
1305
    lines = output.splitlines()
1306

    
1307
    # Try parsing the new output format (ceph >= 0.55).
1308
    splitted_lines = map(lambda l: l.split(), lines)
1309

    
1310
    # Check for empty output.
1311
    if not splitted_lines:
1312
      return None
1313

    
1314
    # Check showmapped output, to determine number of fields.
1315
    field_cnt = len(splitted_lines[0])
1316
    if field_cnt != allfields:
1317
      # Parsing the new format failed. Fallback to parsing the old output
1318
      # format (< 0.55).
1319
      splitted_lines = map(lambda l: l.split("\t"), lines)
1320
      if field_cnt != allfields:
1321
        base.ThrowError("Cannot parse rbd showmapped output expected %s fields,"
1322
                        " found %s", allfields, field_cnt)
1323

    
1324
    matched_lines = \
1325
      filter(lambda l: len(l) == allfields and l[volumefield] == volume_name,
1326
             splitted_lines)
1327

    
1328
    if len(matched_lines) > 1:
1329
      base.ThrowError("rbd volume %s mapped more than once", volume_name)
1330

    
1331
    if matched_lines:
1332
      # rbd block device found. Return it.
1333
      rbd_dev = matched_lines[0][devicefield]
1334
      return rbd_dev
1335

    
1336
    # The given volume is not mapped.
1337
    return None
1338

    
1339
  def Assemble(self):
1340
    """Assemble the device.
1341

1342
    """
1343
    pass
1344

    
1345
  def Shutdown(self):
1346
    """Shutdown the device.
1347

1348
    """
1349
    if not self.minor and not self.Attach():
1350
      # The rbd device doesn't exist.
1351
      return
1352

    
1353
    # Unmap the block device from the Volume.
1354
    self._UnmapVolumeFromBlockdev(self.unique_id)
1355

    
1356
    self.minor = None
1357
    self.dev_path = None
1358

    
1359
  def _UnmapVolumeFromBlockdev(self, unique_id):
1360
    """Unmaps the rbd device from the Volume it is mapped.
1361

1362
    Unmaps the rbd device from the Volume it was previously mapped to.
1363
    This method should be idempotent if the Volume isn't mapped.
1364

1365
    """
1366
    pool = self.params[constants.LDP_POOL]
1367
    name = unique_id[1]
1368

    
1369
    # Check if the mapping already exists.
1370
    rbd_dev = self._VolumeToBlockdev(pool, name)
1371

    
1372
    if rbd_dev:
1373
      # The mapping exists. Unmap the rbd device.
1374
      unmap_cmd = [constants.RBD_CMD, "unmap", "%s" % rbd_dev]
1375
      result = utils.RunCmd(unmap_cmd)
1376
      if result.failed:
1377
        base.ThrowError("rbd unmap failed (%s): %s",
1378
                        result.fail_reason, result.output)
1379

    
1380
  def Open(self, force=False):
1381
    """Make the device ready for I/O.
1382

1383
    """
1384
    pass
1385

    
1386
  def Close(self):
1387
    """Notifies that the device will no longer be used for I/O.
1388

1389
    """
1390
    pass
1391

    
1392
  def Grow(self, amount, dryrun, backingstore, excl_stor):
1393
    """Grow the Volume.
1394

1395
    @type amount: integer
1396
    @param amount: the amount (in mebibytes) to grow with
1397
    @type dryrun: boolean
1398
    @param dryrun: whether to execute the operation in simulation mode
1399
        only, without actually increasing the size
1400

1401
    """
1402
    if not backingstore:
1403
      return
1404
    if not self.Attach():
1405
      base.ThrowError("Can't attach to rbd device during Grow()")
1406

    
1407
    if dryrun:
1408
      # the rbd tool does not support dry runs of resize operations.
1409
      # Since rbd volumes are thinly provisioned, we assume
1410
      # there is always enough free space for the operation.
1411
      return
1412

    
1413
    rbd_pool = self.params[constants.LDP_POOL]
1414
    rbd_name = self.unique_id[1]
1415
    new_size = self.size + amount
1416

    
1417
    # Resize the rbd volume (Image) inside the RADOS cluster.
1418
    cmd = [constants.RBD_CMD, "resize", "-p", rbd_pool,
1419
           rbd_name, "--size", "%s" % new_size]
1420
    result = utils.RunCmd(cmd)
1421
    if result.failed:
1422
      base.ThrowError("rbd resize failed (%s): %s",
1423
                      result.fail_reason, result.output)
1424

    
1425

    
1426
class ExtStorageDevice(base.BlockDev):
1427
  """A block device provided by an ExtStorage Provider.
1428

1429
  This class implements the External Storage Interface, which means
1430
  handling of the externally provided block devices.
1431

1432
  """
1433
  def __init__(self, unique_id, children, size, params):
1434
    """Attaches to an extstorage block device.
1435

1436
    """
1437
    super(ExtStorageDevice, self).__init__(unique_id, children, size, params)
1438
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1439
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1440

    
1441
    self.driver, self.vol_name = unique_id
1442
    self.ext_params = params
1443

    
1444
    self.major = self.minor = None
1445
    self.Attach()
1446

    
1447
  @classmethod
1448
  def Create(cls, unique_id, children, size, spindles, params, excl_stor):
1449
    """Create a new extstorage device.
1450

1451
    Provision a new volume using an extstorage provider, which will
1452
    then be mapped to a block device.
1453

1454
    """
1455
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1456
      raise errors.ProgrammerError("Invalid configuration data %s" %
1457
                                   str(unique_id))
1458
    if excl_stor:
1459
      raise errors.ProgrammerError("extstorage device requested with"
1460
                                   " exclusive_storage")
1461

    
1462
    # Call the External Storage's create script,
1463
    # to provision a new Volume inside the External Storage
1464
    _ExtStorageAction(constants.ES_ACTION_CREATE, unique_id,
1465
                      params, str(size))
1466

    
1467
    return ExtStorageDevice(unique_id, children, size, params)
1468

    
1469
  def Remove(self):
1470
    """Remove the extstorage device.
1471

1472
    """
1473
    if not self.minor and not self.Attach():
1474
      # The extstorage device doesn't exist.
1475
      return
1476

    
1477
    # First shutdown the device (remove mappings).
1478
    self.Shutdown()
1479

    
1480
    # Call the External Storage's remove script,
1481
    # to remove the Volume from the External Storage
1482
    _ExtStorageAction(constants.ES_ACTION_REMOVE, self.unique_id,
1483
                      self.ext_params)
1484

    
1485
  def Rename(self, new_id):
1486
    """Rename this device.
1487

1488
    """
1489
    pass
1490

    
1491
  def Attach(self):
1492
    """Attach to an existing extstorage device.
1493

1494
    This method maps the extstorage volume that matches our name with
1495
    a corresponding block device and then attaches to this device.
1496

1497
    """
1498
    self.attached = False
1499

    
1500
    # Call the External Storage's attach script,
1501
    # to attach an existing Volume to a block device under /dev
1502
    self.dev_path = _ExtStorageAction(constants.ES_ACTION_ATTACH,
1503
                                      self.unique_id, self.ext_params)
1504

    
1505
    try:
1506
      st = os.stat(self.dev_path)
1507
    except OSError, err:
1508
      logging.error("Error stat()'ing %s: %s", self.dev_path, str(err))
1509
      return False
1510

    
1511
    if not stat.S_ISBLK(st.st_mode):
1512
      logging.error("%s is not a block device", self.dev_path)
1513
      return False
1514

    
1515
    self.major = os.major(st.st_rdev)
1516
    self.minor = os.minor(st.st_rdev)
1517
    self.attached = True
1518

    
1519
    return True
1520

    
1521
  def Assemble(self):
1522
    """Assemble the device.
1523

1524
    """
1525
    pass
1526

    
1527
  def Shutdown(self):
1528
    """Shutdown the device.
1529

1530
    """
1531
    if not self.minor and not self.Attach():
1532
      # The extstorage device doesn't exist.
1533
      return
1534

    
1535
    # Call the External Storage's detach script,
1536
    # to detach an existing Volume from it's block device under /dev
1537
    _ExtStorageAction(constants.ES_ACTION_DETACH, self.unique_id,
1538
                      self.ext_params)
1539

    
1540
    self.minor = None
1541
    self.dev_path = None
1542

    
1543
  def Open(self, force=False):
1544
    """Make the device ready for I/O.
1545

1546
    """
1547
    pass
1548

    
1549
  def Close(self):
1550
    """Notifies that the device will no longer be used for I/O.
1551

1552
    """
1553
    pass
1554

    
1555
  def Grow(self, amount, dryrun, backingstore, excl_stor):
1556
    """Grow the Volume.
1557

1558
    @type amount: integer
1559
    @param amount: the amount (in mebibytes) to grow with
1560
    @type dryrun: boolean
1561
    @param dryrun: whether to execute the operation in simulation mode
1562
        only, without actually increasing the size
1563

1564
    """
1565
    if not backingstore:
1566
      return
1567
    if not self.Attach():
1568
      base.ThrowError("Can't attach to extstorage device during Grow()")
1569

    
1570
    if dryrun:
1571
      # we do not support dry runs of resize operations for now.
1572
      return
1573

    
1574
    new_size = self.size + amount
1575

    
1576
    # Call the External Storage's grow script,
1577
    # to grow an existing Volume inside the External Storage
1578
    _ExtStorageAction(constants.ES_ACTION_GROW, self.unique_id,
1579
                      self.ext_params, str(self.size), grow=str(new_size))
1580

    
1581
  def SetInfo(self, text):
1582
    """Update metadata with info text.
1583

1584
    """
1585
    # Replace invalid characters
1586
    text = re.sub("^[^A-Za-z0-9_+.]", "_", text)
1587
    text = re.sub("[^-A-Za-z0-9_+.]", "_", text)
1588

    
1589
    # Only up to 128 characters are allowed
1590
    text = text[:128]
1591

    
1592
    # Call the External Storage's setinfo script,
1593
    # to set metadata for an existing Volume inside the External Storage
1594
    _ExtStorageAction(constants.ES_ACTION_SETINFO, self.unique_id,
1595
                      self.ext_params, metadata=text)
1596

    
1597

    
1598
def _ExtStorageAction(action, unique_id, ext_params,
1599
                      size=None, grow=None, metadata=None):
1600
  """Take an External Storage action.
1601

1602
  Take an External Storage action concerning or affecting
1603
  a specific Volume inside the External Storage.
1604

1605
  @type action: string
1606
  @param action: which action to perform. One of:
1607
                 create / remove / grow / attach / detach
1608
  @type unique_id: tuple (driver, vol_name)
1609
  @param unique_id: a tuple containing the type of ExtStorage (driver)
1610
                    and the Volume name
1611
  @type ext_params: dict
1612
  @param ext_params: ExtStorage parameters
1613
  @type size: integer
1614
  @param size: the size of the Volume in mebibytes
1615
  @type grow: integer
1616
  @param grow: the new size in mebibytes (after grow)
1617
  @type metadata: string
1618
  @param metadata: metadata info of the Volume, for use by the provider
1619
  @rtype: None or a block device path (during attach)
1620

1621
  """
1622
  driver, vol_name = unique_id
1623

    
1624
  # Create an External Storage instance of type `driver'
1625
  status, inst_es = ExtStorageFromDisk(driver)
1626
  if not status:
1627
    base.ThrowError("%s" % inst_es)
1628

    
1629
  # Create the basic environment for the driver's scripts
1630
  create_env = _ExtStorageEnvironment(unique_id, ext_params, size,
1631
                                      grow, metadata)
1632

    
1633
  # Do not use log file for action `attach' as we need
1634
  # to get the output from RunResult
1635
  # TODO: find a way to have a log file for attach too
1636
  logfile = None
1637
  if action is not constants.ES_ACTION_ATTACH:
1638
    logfile = _VolumeLogName(action, driver, vol_name)
1639

    
1640
  # Make sure the given action results in a valid script
1641
  if action not in constants.ES_SCRIPTS:
1642
    base.ThrowError("Action '%s' doesn't result in a valid ExtStorage script" %
1643
                    action)
1644

    
1645
  # Find out which external script to run according the given action
1646
  script_name = action + "_script"
1647
  script = getattr(inst_es, script_name)
1648

    
1649
  # Run the external script
1650
  result = utils.RunCmd([script], env=create_env,
1651
                        cwd=inst_es.path, output=logfile,)
1652
  if result.failed:
1653
    logging.error("External storage's %s command '%s' returned"
1654
                  " error: %s, logfile: %s, output: %s",
1655
                  action, result.cmd, result.fail_reason,
1656
                  logfile, result.output)
1657

    
1658
    # If logfile is 'None' (during attach), it breaks TailFile
1659
    # TODO: have a log file for attach too
1660
    if action is not constants.ES_ACTION_ATTACH:
1661
      lines = [utils.SafeEncode(val)
1662
               for val in utils.TailFile(logfile, lines=20)]
1663
    else:
1664
      lines = result.output[-20:]
1665

    
1666
    base.ThrowError("External storage's %s script failed (%s), last"
1667
                    " lines of output:\n%s",
1668
                    action, result.fail_reason, "\n".join(lines))
1669

    
1670
  if action == constants.ES_ACTION_ATTACH:
1671
    return result.stdout
1672

    
1673

    
1674
def ExtStorageFromDisk(name, base_dir=None):
1675
  """Create an ExtStorage instance from disk.
1676

1677
  This function will return an ExtStorage instance
1678
  if the given name is a valid ExtStorage name.
1679

1680
  @type base_dir: string
1681
  @keyword base_dir: Base directory containing ExtStorage installations.
1682
                     Defaults to a search in all the ES_SEARCH_PATH dirs.
1683
  @rtype: tuple
1684
  @return: True and the ExtStorage instance if we find a valid one, or
1685
      False and the diagnose message on error
1686

1687
  """
1688
  if base_dir is None:
1689
    es_base_dir = pathutils.ES_SEARCH_PATH
1690
  else:
1691
    es_base_dir = [base_dir]
1692

    
1693
  es_dir = utils.FindFile(name, es_base_dir, os.path.isdir)
1694

    
1695
  if es_dir is None:
1696
    return False, ("Directory for External Storage Provider %s not"
1697
                   " found in search path" % name)
1698

    
1699
  # ES Files dictionary, we will populate it with the absolute path
1700
  # names; if the value is True, then it is a required file, otherwise
1701
  # an optional one
1702
  es_files = dict.fromkeys(constants.ES_SCRIPTS, True)
1703

    
1704
  es_files[constants.ES_PARAMETERS_FILE] = True
1705

    
1706
  for (filename, _) in es_files.items():
1707
    es_files[filename] = utils.PathJoin(es_dir, filename)
1708

    
1709
    try:
1710
      st = os.stat(es_files[filename])
1711
    except EnvironmentError, err:
1712
      return False, ("File '%s' under path '%s' is missing (%s)" %
1713
                     (filename, es_dir, utils.ErrnoOrStr(err)))
1714

    
1715
    if not stat.S_ISREG(stat.S_IFMT(st.st_mode)):
1716
      return False, ("File '%s' under path '%s' is not a regular file" %
1717
                     (filename, es_dir))
1718

    
1719
    if filename in constants.ES_SCRIPTS:
1720
      if stat.S_IMODE(st.st_mode) & stat.S_IXUSR != stat.S_IXUSR:
1721
        return False, ("File '%s' under path '%s' is not executable" %
1722
                       (filename, es_dir))
1723

    
1724
  parameters = []
1725
  if constants.ES_PARAMETERS_FILE in es_files:
1726
    parameters_file = es_files[constants.ES_PARAMETERS_FILE]
1727
    try:
1728
      parameters = utils.ReadFile(parameters_file).splitlines()
1729
    except EnvironmentError, err:
1730
      return False, ("Error while reading the EXT parameters file at %s: %s" %
1731
                     (parameters_file, utils.ErrnoOrStr(err)))
1732
    parameters = [v.split(None, 1) for v in parameters]
1733

    
1734
  es_obj = \
1735
    objects.ExtStorage(name=name, path=es_dir,
1736
                       create_script=es_files[constants.ES_SCRIPT_CREATE],
1737
                       remove_script=es_files[constants.ES_SCRIPT_REMOVE],
1738
                       grow_script=es_files[constants.ES_SCRIPT_GROW],
1739
                       attach_script=es_files[constants.ES_SCRIPT_ATTACH],
1740
                       detach_script=es_files[constants.ES_SCRIPT_DETACH],
1741
                       setinfo_script=es_files[constants.ES_SCRIPT_SETINFO],
1742
                       verify_script=es_files[constants.ES_SCRIPT_VERIFY],
1743
                       supported_parameters=parameters)
1744
  return True, es_obj
1745

    
1746

    
1747
def _ExtStorageEnvironment(unique_id, ext_params,
1748
                           size=None, grow=None, metadata=None):
1749
  """Calculate the environment for an External Storage script.
1750

1751
  @type unique_id: tuple (driver, vol_name)
1752
  @param unique_id: ExtStorage pool and name of the Volume
1753
  @type ext_params: dict
1754
  @param ext_params: the EXT parameters
1755
  @type size: string
1756
  @param size: size of the Volume (in mebibytes)
1757
  @type grow: string
1758
  @param grow: new size of Volume after grow (in mebibytes)
1759
  @type metadata: string
1760
  @param metadata: metadata info of the Volume
1761
  @rtype: dict
1762
  @return: dict of environment variables
1763

1764
  """
1765
  vol_name = unique_id[1]
1766

    
1767
  result = {}
1768
  result["VOL_NAME"] = vol_name
1769

    
1770
  # EXT params
1771
  for pname, pvalue in ext_params.items():
1772
    result["EXTP_%s" % pname.upper()] = str(pvalue)
1773

    
1774
  if size is not None:
1775
    result["VOL_SIZE"] = size
1776

    
1777
  if grow is not None:
1778
    result["VOL_NEW_SIZE"] = grow
1779

    
1780
  if metadata is not None:
1781
    result["VOL_METADATA"] = metadata
1782

    
1783
  return result
1784

    
1785

    
1786
def _VolumeLogName(kind, es_name, volume):
1787
  """Compute the ExtStorage log filename for a given Volume and operation.
1788

1789
  @type kind: string
1790
  @param kind: the operation type (e.g. create, remove etc.)
1791
  @type es_name: string
1792
  @param es_name: the ExtStorage name
1793
  @type volume: string
1794
  @param volume: the name of the Volume inside the External Storage
1795

1796
  """
1797
  # Check if the extstorage log dir is a valid dir
1798
  if not os.path.isdir(pathutils.LOG_ES_DIR):
1799
    base.ThrowError("Cannot find log directory: %s", pathutils.LOG_ES_DIR)
1800

    
1801
  # TODO: Use tempfile.mkstemp to create unique filename
1802
  basename = ("%s-%s-%s-%s.log" %
1803
              (kind, es_name, volume, utils.TimestampForFilename()))
1804
  return utils.PathJoin(pathutils.LOG_ES_DIR, basename)
1805

    
1806

    
1807
DEV_MAP = {
1808
  constants.LD_LV: LogicalVolume,
1809
  constants.LD_DRBD8: drbd.DRBD8Dev,
1810
  constants.LD_BLOCKDEV: PersistentBlockDevice,
1811
  constants.LD_RBD: RADOSBlockDevice,
1812
  constants.LD_EXT: ExtStorageDevice,
1813
  }
1814

    
1815
if constants.ENABLE_FILE_STORAGE or constants.ENABLE_SHARED_FILE_STORAGE:
1816
  DEV_MAP[constants.LD_FILE] = FileStorage
1817

    
1818

    
1819
def _VerifyDiskType(dev_type):
1820
  if dev_type not in DEV_MAP:
1821
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1822

    
1823

    
1824
def _VerifyDiskParams(disk):
1825
  """Verifies if all disk parameters are set.
1826

1827
  """
1828
  missing = set(constants.DISK_LD_DEFAULTS[disk.dev_type]) - set(disk.params)
1829
  if missing:
1830
    raise errors.ProgrammerError("Block device is missing disk parameters: %s" %
1831
                                 missing)
1832

    
1833

    
1834
def FindDevice(disk, children):
1835
  """Search for an existing, assembled device.
1836

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

1840
  @type disk: L{objects.Disk}
1841
  @param disk: the disk object to find
1842
  @type children: list of L{bdev.BlockDev}
1843
  @param children: the list of block devices that are children of the device
1844
                  represented by the disk parameter
1845

1846
  """
1847
  _VerifyDiskType(disk.dev_type)
1848
  device = DEV_MAP[disk.dev_type](disk.physical_id, children, disk.size,
1849
                                  disk.params)
1850
  if not device.attached:
1851
    return None
1852
  return device
1853

    
1854

    
1855
def Assemble(disk, children):
1856
  """Try to attach or assemble an existing device.
1857

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

1861
  @type disk: L{objects.Disk}
1862
  @param disk: the disk object to assemble
1863
  @type children: list of L{bdev.BlockDev}
1864
  @param children: the list of block devices that are children of the device
1865
                  represented by the disk parameter
1866

1867
  """
1868
  _VerifyDiskType(disk.dev_type)
1869
  _VerifyDiskParams(disk)
1870
  device = DEV_MAP[disk.dev_type](disk.physical_id, children, disk.size,
1871
                                  disk.params)
1872
  device.Assemble()
1873
  return device
1874

    
1875

    
1876
def Create(disk, children, excl_stor):
1877
  """Create a device.
1878

1879
  @type disk: L{objects.Disk}
1880
  @param disk: the disk object to create
1881
  @type children: list of L{bdev.BlockDev}
1882
  @param children: the list of block devices that are children of the device
1883
                  represented by the disk parameter
1884
  @type excl_stor: boolean
1885
  @param excl_stor: Whether exclusive_storage is active
1886
  @rtype: L{bdev.BlockDev}
1887
  @return: the created device, or C{None} in case of an error
1888

1889
  """
1890
  _VerifyDiskType(disk.dev_type)
1891
  _VerifyDiskParams(disk)
1892
  device = DEV_MAP[disk.dev_type].Create(disk.physical_id, children, disk.size,
1893
                                         disk.spindles, disk.params, excl_stor)
1894
  return device