Statistics
| Branch: | Tag: | Revision:

root / lib / storage / bdev.py @ 13669ecd

History | View | Annotate | Download (58.2 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):
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
    # we try multiple algorithms since the 'best' ones might not have
775
    # space available in the right place, but later ones might (since
776
    # they have less constraints); also note that only recent LVM
777
    # supports 'cling'
778
    for alloc_policy in "contiguous", "cling", "normal":
779
      result = utils.RunCmd(cmd + ["--alloc", alloc_policy, self.dev_path])
780
      if not result.failed:
781
        return
782
    base.ThrowError("Can't grow LV %s: %s", self.dev_path, result.output)
783

    
784
  def GetActualSpindles(self):
785
    """Return the number of spindles used.
786

787
    """
788
    assert self.attached, "BlockDevice not attached in GetActualSpindles()"
789
    return len(self.pv_names)
790

    
791

    
792
class FileStorage(base.BlockDev):
793
  """File device.
794

795
  This class represents the a file storage backend device.
796

797
  The unique_id for the file device is a (file_driver, file_path) tuple.
798

799
  """
800
  def __init__(self, unique_id, children, size, params):
801
    """Initalizes a file device backend.
802

803
    """
804
    if children:
805
      raise errors.BlockDeviceError("Invalid setup for file device")
806
    super(FileStorage, self).__init__(unique_id, children, size, params)
807
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
808
      raise ValueError("Invalid configuration data %s" % str(unique_id))
809
    self.driver = unique_id[0]
810
    self.dev_path = unique_id[1]
811

    
812
    CheckFileStoragePath(self.dev_path)
813

    
814
    self.Attach()
815

    
816
  def Assemble(self):
817
    """Assemble the device.
818

819
    Checks whether the file device exists, raises BlockDeviceError otherwise.
820

821
    """
822
    if not os.path.exists(self.dev_path):
823
      base.ThrowError("File device '%s' does not exist" % self.dev_path)
824

    
825
  def Shutdown(self):
826
    """Shutdown the device.
827

828
    This is a no-op for the file type, as we don't deactivate
829
    the file on shutdown.
830

831
    """
832
    pass
833

    
834
  def Open(self, force=False):
835
    """Make the device ready for I/O.
836

837
    This is a no-op for the file type.
838

839
    """
840
    pass
841

    
842
  def Close(self):
843
    """Notifies that the device will no longer be used for I/O.
844

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

847
    """
848
    pass
849

    
850
  def Remove(self):
851
    """Remove the file backing the block device.
852

853
    @rtype: boolean
854
    @return: True if the removal was successful
855

856
    """
857
    try:
858
      os.remove(self.dev_path)
859
    except OSError, err:
860
      if err.errno != errno.ENOENT:
861
        base.ThrowError("Can't remove file '%s': %s", self.dev_path, err)
862

    
863
  def Rename(self, new_id):
864
    """Renames the file.
865

866
    """
867
    # TODO: implement rename for file-based storage
868
    base.ThrowError("Rename is not supported for file-based storage")
869

    
870
  def Grow(self, amount, dryrun, backingstore):
871
    """Grow the file
872

873
    @param amount: the amount (in mebibytes) to grow with
874

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

    
893
  def Attach(self):
894
    """Attach to an existing file.
895

896
    Check if this file already exists.
897

898
    @rtype: boolean
899
    @return: True if file exists
900

901
    """
902
    self.attached = os.path.exists(self.dev_path)
903
    return self.attached
904

    
905
  def GetActualSize(self):
906
    """Return the actual disk size.
907

908
    @note: the device needs to be active when this is called
909

910
    """
911
    assert self.attached, "BlockDevice not attached in GetActualSize()"
912
    try:
913
      st = os.stat(self.dev_path)
914
      return st.st_size
915
    except OSError, err:
916
      base.ThrowError("Can't stat %s: %s", self.dev_path, err)
917

    
918
  @classmethod
919
  def Create(cls, unique_id, children, size, spindles, params, excl_stor):
920
    """Create a new file.
921

922
    @param size: the size of file in MiB
923

924
    @rtype: L{bdev.FileStorage}
925
    @return: an instance of FileStorage
926

927
    """
928
    if excl_stor:
929
      raise errors.ProgrammerError("FileStorage device requested with"
930
                                   " exclusive_storage")
931
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
932
      raise ValueError("Invalid configuration data %s" % str(unique_id))
933

    
934
    dev_path = unique_id[1]
935

    
936
    CheckFileStoragePath(dev_path)
937

    
938
    try:
939
      fd = os.open(dev_path, os.O_RDWR | os.O_CREAT | os.O_EXCL)
940
      f = os.fdopen(fd, "w")
941
      f.truncate(size * 1024 * 1024)
942
      f.close()
943
    except EnvironmentError, err:
944
      if err.errno == errno.EEXIST:
945
        base.ThrowError("File already existing: %s", dev_path)
946
      base.ThrowError("Error in file creation: %", str(err))
947

    
948
    return FileStorage(unique_id, children, size, params)
949

    
950

    
951
class PersistentBlockDevice(base.BlockDev):
952
  """A block device with persistent node
953

954
  May be either directly attached, or exposed through DM (e.g. dm-multipath).
955
  udev helpers are probably required to give persistent, human-friendly
956
  names.
957

958
  For the time being, pathnames are required to lie under /dev.
959

960
  """
961
  def __init__(self, unique_id, children, size, params):
962
    """Attaches to a static block device.
963

964
    The unique_id is a path under /dev.
965

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

    
983
    self.major = self.minor = None
984
    self.Attach()
985

    
986
  @classmethod
987
  def Create(cls, unique_id, children, size, spindles, params, excl_stor):
988
    """Create a new device
989

990
    This is a noop, we only return a PersistentBlockDevice instance
991

992
    """
993
    if excl_stor:
994
      raise errors.ProgrammerError("Persistent block device requested with"
995
                                   " exclusive_storage")
996
    return PersistentBlockDevice(unique_id, children, 0, params)
997

    
998
  def Remove(self):
999
    """Remove a device
1000

1001
    This is a noop
1002

1003
    """
1004
    pass
1005

    
1006
  def Rename(self, new_id):
1007
    """Rename this device.
1008

1009
    """
1010
    base.ThrowError("Rename is not supported for PersistentBlockDev storage")
1011

    
1012
  def Attach(self):
1013
    """Attach to an existing block device.
1014

1015

1016
    """
1017
    self.attached = False
1018
    try:
1019
      st = os.stat(self.dev_path)
1020
    except OSError, err:
1021
      logging.error("Error stat()'ing %s: %s", self.dev_path, str(err))
1022
      return False
1023

    
1024
    if not stat.S_ISBLK(st.st_mode):
1025
      logging.error("%s is not a block device", self.dev_path)
1026
      return False
1027

    
1028
    self.major = os.major(st.st_rdev)
1029
    self.minor = os.minor(st.st_rdev)
1030
    self.attached = True
1031

    
1032
    return True
1033

    
1034
  def Assemble(self):
1035
    """Assemble the device.
1036

1037
    """
1038
    pass
1039

    
1040
  def Shutdown(self):
1041
    """Shutdown the device.
1042

1043
    """
1044
    pass
1045

    
1046
  def Open(self, force=False):
1047
    """Make the device ready for I/O.
1048

1049
    """
1050
    pass
1051

    
1052
  def Close(self):
1053
    """Notifies that the device will no longer be used for I/O.
1054

1055
    """
1056
    pass
1057

    
1058
  def Grow(self, amount, dryrun, backingstore):
1059
    """Grow the logical volume.
1060

1061
    """
1062
    base.ThrowError("Grow is not supported for PersistentBlockDev storage")
1063

    
1064

    
1065
class RADOSBlockDevice(base.BlockDev):
1066
  """A RADOS Block Device (rbd).
1067

1068
  This class implements the RADOS Block Device for the backend. You need
1069
  the rbd kernel driver, the RADOS Tools and a working RADOS cluster for
1070
  this to be functional.
1071

1072
  """
1073
  def __init__(self, unique_id, children, size, params):
1074
    """Attaches to an rbd device.
1075

1076
    """
1077
    super(RADOSBlockDevice, self).__init__(unique_id, children, size, params)
1078
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1079
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1080

    
1081
    self.driver, self.rbd_name = unique_id
1082

    
1083
    self.major = self.minor = None
1084
    self.Attach()
1085

    
1086
  @classmethod
1087
  def Create(cls, unique_id, children, size, spindles, params, excl_stor):
1088
    """Create a new rbd device.
1089

1090
    Provision a new rbd volume inside a RADOS pool.
1091

1092
    """
1093
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1094
      raise errors.ProgrammerError("Invalid configuration data %s" %
1095
                                   str(unique_id))
1096
    if excl_stor:
1097
      raise errors.ProgrammerError("RBD device requested with"
1098
                                   " exclusive_storage")
1099
    rbd_pool = params[constants.LDP_POOL]
1100
    rbd_name = unique_id[1]
1101

    
1102
    # Provision a new rbd volume (Image) inside the RADOS cluster.
1103
    cmd = [constants.RBD_CMD, "create", "-p", rbd_pool,
1104
           rbd_name, "--size", "%s" % size]
1105
    result = utils.RunCmd(cmd)
1106
    if result.failed:
1107
      base.ThrowError("rbd creation failed (%s): %s",
1108
                      result.fail_reason, result.output)
1109

    
1110
    return RADOSBlockDevice(unique_id, children, size, params)
1111

    
1112
  def Remove(self):
1113
    """Remove the rbd device.
1114

1115
    """
1116
    rbd_pool = self.params[constants.LDP_POOL]
1117
    rbd_name = self.unique_id[1]
1118

    
1119
    if not self.minor and not self.Attach():
1120
      # The rbd device doesn't exist.
1121
      return
1122

    
1123
    # First shutdown the device (remove mappings).
1124
    self.Shutdown()
1125

    
1126
    # Remove the actual Volume (Image) from the RADOS cluster.
1127
    cmd = [constants.RBD_CMD, "rm", "-p", rbd_pool, rbd_name]
1128
    result = utils.RunCmd(cmd)
1129
    if result.failed:
1130
      base.ThrowError("Can't remove Volume from cluster with rbd rm: %s - %s",
1131
                      result.fail_reason, result.output)
1132

    
1133
  def Rename(self, new_id):
1134
    """Rename this device.
1135

1136
    """
1137
    pass
1138

    
1139
  def Attach(self):
1140
    """Attach to an existing rbd device.
1141

1142
    This method maps the rbd volume that matches our name with
1143
    an rbd device and then attaches to this device.
1144

1145
    """
1146
    self.attached = False
1147

    
1148
    # Map the rbd volume to a block device under /dev
1149
    self.dev_path = self._MapVolumeToBlockdev(self.unique_id)
1150

    
1151
    try:
1152
      st = os.stat(self.dev_path)
1153
    except OSError, err:
1154
      logging.error("Error stat()'ing %s: %s", self.dev_path, str(err))
1155
      return False
1156

    
1157
    if not stat.S_ISBLK(st.st_mode):
1158
      logging.error("%s is not a block device", self.dev_path)
1159
      return False
1160

    
1161
    self.major = os.major(st.st_rdev)
1162
    self.minor = os.minor(st.st_rdev)
1163
    self.attached = True
1164

    
1165
    return True
1166

    
1167
  def _MapVolumeToBlockdev(self, unique_id):
1168
    """Maps existing rbd volumes to block devices.
1169

1170
    This method should be idempotent if the mapping already exists.
1171

1172
    @rtype: string
1173
    @return: the block device path that corresponds to the volume
1174

1175
    """
1176
    pool = self.params[constants.LDP_POOL]
1177
    name = unique_id[1]
1178

    
1179
    # Check if the mapping already exists.
1180
    rbd_dev = self._VolumeToBlockdev(pool, name)
1181
    if rbd_dev:
1182
      # The mapping exists. Return it.
1183
      return rbd_dev
1184

    
1185
    # The mapping doesn't exist. Create it.
1186
    map_cmd = [constants.RBD_CMD, "map", "-p", pool, name]
1187
    result = utils.RunCmd(map_cmd)
1188
    if result.failed:
1189
      base.ThrowError("rbd map failed (%s): %s",
1190
                      result.fail_reason, result.output)
1191

    
1192
    # Find the corresponding rbd device.
1193
    rbd_dev = self._VolumeToBlockdev(pool, name)
1194
    if not rbd_dev:
1195
      base.ThrowError("rbd map succeeded, but could not find the rbd block"
1196
                      " device in output of showmapped, for volume: %s", name)
1197

    
1198
    # The device was successfully mapped. Return it.
1199
    return rbd_dev
1200

    
1201
  @classmethod
1202
  def _VolumeToBlockdev(cls, pool, volume_name):
1203
    """Do the 'volume name'-to-'rbd block device' resolving.
1204

1205
    @type pool: string
1206
    @param pool: RADOS pool to use
1207
    @type volume_name: string
1208
    @param volume_name: the name of the volume whose device we search for
1209
    @rtype: string or None
1210
    @return: block device path if the volume is mapped, else None
1211

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

    
1231
      return cls._ParseRbdShowmappedJson(result.output, volume_name)
1232
    except RbdShowmappedJsonError:
1233
      # For older versions of rbd, we have to parse the plain / text output
1234
      # manually.
1235
      showmap_cmd = [constants.RBD_CMD, "showmapped", "-p", pool]
1236
      result = utils.RunCmd(showmap_cmd)
1237
      if result.failed:
1238
        base.ThrowError("rbd showmapped failed (%s): %s",
1239
                        result.fail_reason, result.output)
1240

    
1241
      return cls._ParseRbdShowmappedPlain(result.output, volume_name)
1242

    
1243
  @staticmethod
1244
  def _ParseRbdShowmappedJson(output, volume_name):
1245
    """Parse the json output of `rbd showmapped'.
1246

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

1250
    @type output: string
1251
    @param output: the json output of `rbd showmapped'
1252
    @type volume_name: string
1253
    @param volume_name: the name of the volume whose device we search for
1254
    @rtype: string or None
1255
    @return: block device path if the volume is mapped, else None
1256

1257
    """
1258
    try:
1259
      devices = serializer.LoadJson(output)
1260
    except ValueError, err:
1261
      base.ThrowError("Unable to parse JSON data: %s" % err)
1262

    
1263
    rbd_dev = None
1264
    for d in devices.values(): # pylint: disable=E1103
1265
      try:
1266
        name = d["name"]
1267
      except KeyError:
1268
        base.ThrowError("'name' key missing from json object %s", devices)
1269

    
1270
      if name == volume_name:
1271
        if rbd_dev is not None:
1272
          base.ThrowError("rbd volume %s is mapped more than once", volume_name)
1273

    
1274
        rbd_dev = d["device"]
1275

    
1276
    return rbd_dev
1277

    
1278
  @staticmethod
1279
  def _ParseRbdShowmappedPlain(output, volume_name):
1280
    """Parse the (plain / text) output of `rbd showmapped'.
1281

1282
    This method parses the output of `rbd showmapped' and returns
1283
    the rbd block device path (e.g. /dev/rbd0) that matches the
1284
    given rbd volume.
1285

1286
    @type output: string
1287
    @param output: the plain text output of `rbd showmapped'
1288
    @type volume_name: string
1289
    @param volume_name: the name of the volume whose device we search for
1290
    @rtype: string or None
1291
    @return: block device path if the volume is mapped, else None
1292

1293
    """
1294
    allfields = 5
1295
    volumefield = 2
1296
    devicefield = 4
1297

    
1298
    lines = output.splitlines()
1299

    
1300
    # Try parsing the new output format (ceph >= 0.55).
1301
    splitted_lines = map(lambda l: l.split(), lines)
1302

    
1303
    # Check for empty output.
1304
    if not splitted_lines:
1305
      return None
1306

    
1307
    # Check showmapped output, to determine number of fields.
1308
    field_cnt = len(splitted_lines[0])
1309
    if field_cnt != allfields:
1310
      # Parsing the new format failed. Fallback to parsing the old output
1311
      # format (< 0.55).
1312
      splitted_lines = map(lambda l: l.split("\t"), lines)
1313
      if field_cnt != allfields:
1314
        base.ThrowError("Cannot parse rbd showmapped output expected %s fields,"
1315
                        " found %s", allfields, field_cnt)
1316

    
1317
    matched_lines = \
1318
      filter(lambda l: len(l) == allfields and l[volumefield] == volume_name,
1319
             splitted_lines)
1320

    
1321
    if len(matched_lines) > 1:
1322
      base.ThrowError("rbd volume %s mapped more than once", volume_name)
1323

    
1324
    if matched_lines:
1325
      # rbd block device found. Return it.
1326
      rbd_dev = matched_lines[0][devicefield]
1327
      return rbd_dev
1328

    
1329
    # The given volume is not mapped.
1330
    return None
1331

    
1332
  def Assemble(self):
1333
    """Assemble the device.
1334

1335
    """
1336
    pass
1337

    
1338
  def Shutdown(self):
1339
    """Shutdown the device.
1340

1341
    """
1342
    if not self.minor and not self.Attach():
1343
      # The rbd device doesn't exist.
1344
      return
1345

    
1346
    # Unmap the block device from the Volume.
1347
    self._UnmapVolumeFromBlockdev(self.unique_id)
1348

    
1349
    self.minor = None
1350
    self.dev_path = None
1351

    
1352
  def _UnmapVolumeFromBlockdev(self, unique_id):
1353
    """Unmaps the rbd device from the Volume it is mapped.
1354

1355
    Unmaps the rbd device from the Volume it was previously mapped to.
1356
    This method should be idempotent if the Volume isn't mapped.
1357

1358
    """
1359
    pool = self.params[constants.LDP_POOL]
1360
    name = unique_id[1]
1361

    
1362
    # Check if the mapping already exists.
1363
    rbd_dev = self._VolumeToBlockdev(pool, name)
1364

    
1365
    if rbd_dev:
1366
      # The mapping exists. Unmap the rbd device.
1367
      unmap_cmd = [constants.RBD_CMD, "unmap", "%s" % rbd_dev]
1368
      result = utils.RunCmd(unmap_cmd)
1369
      if result.failed:
1370
        base.ThrowError("rbd unmap failed (%s): %s",
1371
                        result.fail_reason, result.output)
1372

    
1373
  def Open(self, force=False):
1374
    """Make the device ready for I/O.
1375

1376
    """
1377
    pass
1378

    
1379
  def Close(self):
1380
    """Notifies that the device will no longer be used for I/O.
1381

1382
    """
1383
    pass
1384

    
1385
  def Grow(self, amount, dryrun, backingstore):
1386
    """Grow the Volume.
1387

1388
    @type amount: integer
1389
    @param amount: the amount (in mebibytes) to grow with
1390
    @type dryrun: boolean
1391
    @param dryrun: whether to execute the operation in simulation mode
1392
        only, without actually increasing the size
1393

1394
    """
1395
    if not backingstore:
1396
      return
1397
    if not self.Attach():
1398
      base.ThrowError("Can't attach to rbd device during Grow()")
1399

    
1400
    if dryrun:
1401
      # the rbd tool does not support dry runs of resize operations.
1402
      # Since rbd volumes are thinly provisioned, we assume
1403
      # there is always enough free space for the operation.
1404
      return
1405

    
1406
    rbd_pool = self.params[constants.LDP_POOL]
1407
    rbd_name = self.unique_id[1]
1408
    new_size = self.size + amount
1409

    
1410
    # Resize the rbd volume (Image) inside the RADOS cluster.
1411
    cmd = [constants.RBD_CMD, "resize", "-p", rbd_pool,
1412
           rbd_name, "--size", "%s" % new_size]
1413
    result = utils.RunCmd(cmd)
1414
    if result.failed:
1415
      base.ThrowError("rbd resize failed (%s): %s",
1416
                      result.fail_reason, result.output)
1417

    
1418

    
1419
class ExtStorageDevice(base.BlockDev):
1420
  """A block device provided by an ExtStorage Provider.
1421

1422
  This class implements the External Storage Interface, which means
1423
  handling of the externally provided block devices.
1424

1425
  """
1426
  def __init__(self, unique_id, children, size, params):
1427
    """Attaches to an extstorage block device.
1428

1429
    """
1430
    super(ExtStorageDevice, self).__init__(unique_id, children, size, params)
1431
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1432
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1433

    
1434
    self.driver, self.vol_name = unique_id
1435
    self.ext_params = params
1436

    
1437
    self.major = self.minor = None
1438
    self.Attach()
1439

    
1440
  @classmethod
1441
  def Create(cls, unique_id, children, size, spindles, params, excl_stor):
1442
    """Create a new extstorage device.
1443

1444
    Provision a new volume using an extstorage provider, which will
1445
    then be mapped to a block device.
1446

1447
    """
1448
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1449
      raise errors.ProgrammerError("Invalid configuration data %s" %
1450
                                   str(unique_id))
1451
    if excl_stor:
1452
      raise errors.ProgrammerError("extstorage device requested with"
1453
                                   " exclusive_storage")
1454

    
1455
    # Call the External Storage's create script,
1456
    # to provision a new Volume inside the External Storage
1457
    _ExtStorageAction(constants.ES_ACTION_CREATE, unique_id,
1458
                      params, str(size))
1459

    
1460
    return ExtStorageDevice(unique_id, children, size, params)
1461

    
1462
  def Remove(self):
1463
    """Remove the extstorage device.
1464

1465
    """
1466
    if not self.minor and not self.Attach():
1467
      # The extstorage device doesn't exist.
1468
      return
1469

    
1470
    # First shutdown the device (remove mappings).
1471
    self.Shutdown()
1472

    
1473
    # Call the External Storage's remove script,
1474
    # to remove the Volume from the External Storage
1475
    _ExtStorageAction(constants.ES_ACTION_REMOVE, self.unique_id,
1476
                      self.ext_params)
1477

    
1478
  def Rename(self, new_id):
1479
    """Rename this device.
1480

1481
    """
1482
    pass
1483

    
1484
  def Attach(self):
1485
    """Attach to an existing extstorage device.
1486

1487
    This method maps the extstorage volume that matches our name with
1488
    a corresponding block device and then attaches to this device.
1489

1490
    """
1491
    self.attached = False
1492

    
1493
    # Call the External Storage's attach script,
1494
    # to attach an existing Volume to a block device under /dev
1495
    self.dev_path = _ExtStorageAction(constants.ES_ACTION_ATTACH,
1496
                                      self.unique_id, self.ext_params)
1497

    
1498
    try:
1499
      st = os.stat(self.dev_path)
1500
    except OSError, err:
1501
      logging.error("Error stat()'ing %s: %s", self.dev_path, str(err))
1502
      return False
1503

    
1504
    if not stat.S_ISBLK(st.st_mode):
1505
      logging.error("%s is not a block device", self.dev_path)
1506
      return False
1507

    
1508
    self.major = os.major(st.st_rdev)
1509
    self.minor = os.minor(st.st_rdev)
1510
    self.attached = True
1511

    
1512
    return True
1513

    
1514
  def Assemble(self):
1515
    """Assemble the device.
1516

1517
    """
1518
    pass
1519

    
1520
  def Shutdown(self):
1521
    """Shutdown the device.
1522

1523
    """
1524
    if not self.minor and not self.Attach():
1525
      # The extstorage device doesn't exist.
1526
      return
1527

    
1528
    # Call the External Storage's detach script,
1529
    # to detach an existing Volume from it's block device under /dev
1530
    _ExtStorageAction(constants.ES_ACTION_DETACH, self.unique_id,
1531
                      self.ext_params)
1532

    
1533
    self.minor = None
1534
    self.dev_path = None
1535

    
1536
  def Open(self, force=False):
1537
    """Make the device ready for I/O.
1538

1539
    """
1540
    pass
1541

    
1542
  def Close(self):
1543
    """Notifies that the device will no longer be used for I/O.
1544

1545
    """
1546
    pass
1547

    
1548
  def Grow(self, amount, dryrun, backingstore):
1549
    """Grow the Volume.
1550

1551
    @type amount: integer
1552
    @param amount: the amount (in mebibytes) to grow with
1553
    @type dryrun: boolean
1554
    @param dryrun: whether to execute the operation in simulation mode
1555
        only, without actually increasing the size
1556

1557
    """
1558
    if not backingstore:
1559
      return
1560
    if not self.Attach():
1561
      base.ThrowError("Can't attach to extstorage device during Grow()")
1562

    
1563
    if dryrun:
1564
      # we do not support dry runs of resize operations for now.
1565
      return
1566

    
1567
    new_size = self.size + amount
1568

    
1569
    # Call the External Storage's grow script,
1570
    # to grow an existing Volume inside the External Storage
1571
    _ExtStorageAction(constants.ES_ACTION_GROW, self.unique_id,
1572
                      self.ext_params, str(self.size), grow=str(new_size))
1573

    
1574
  def SetInfo(self, text):
1575
    """Update metadata with info text.
1576

1577
    """
1578
    # Replace invalid characters
1579
    text = re.sub("^[^A-Za-z0-9_+.]", "_", text)
1580
    text = re.sub("[^-A-Za-z0-9_+.]", "_", text)
1581

    
1582
    # Only up to 128 characters are allowed
1583
    text = text[:128]
1584

    
1585
    # Call the External Storage's setinfo script,
1586
    # to set metadata for an existing Volume inside the External Storage
1587
    _ExtStorageAction(constants.ES_ACTION_SETINFO, self.unique_id,
1588
                      self.ext_params, metadata=text)
1589

    
1590

    
1591
def _ExtStorageAction(action, unique_id, ext_params,
1592
                      size=None, grow=None, metadata=None):
1593
  """Take an External Storage action.
1594

1595
  Take an External Storage action concerning or affecting
1596
  a specific Volume inside the External Storage.
1597

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

1614
  """
1615
  driver, vol_name = unique_id
1616

    
1617
  # Create an External Storage instance of type `driver'
1618
  status, inst_es = ExtStorageFromDisk(driver)
1619
  if not status:
1620
    base.ThrowError("%s" % inst_es)
1621

    
1622
  # Create the basic environment for the driver's scripts
1623
  create_env = _ExtStorageEnvironment(unique_id, ext_params, size,
1624
                                      grow, metadata)
1625

    
1626
  # Do not use log file for action `attach' as we need
1627
  # to get the output from RunResult
1628
  # TODO: find a way to have a log file for attach too
1629
  logfile = None
1630
  if action is not constants.ES_ACTION_ATTACH:
1631
    logfile = _VolumeLogName(action, driver, vol_name)
1632

    
1633
  # Make sure the given action results in a valid script
1634
  if action not in constants.ES_SCRIPTS:
1635
    base.ThrowError("Action '%s' doesn't result in a valid ExtStorage script" %
1636
                    action)
1637

    
1638
  # Find out which external script to run according the given action
1639
  script_name = action + "_script"
1640
  script = getattr(inst_es, script_name)
1641

    
1642
  # Run the external script
1643
  result = utils.RunCmd([script], env=create_env,
1644
                        cwd=inst_es.path, output=logfile,)
1645
  if result.failed:
1646
    logging.error("External storage's %s command '%s' returned"
1647
                  " error: %s, logfile: %s, output: %s",
1648
                  action, result.cmd, result.fail_reason,
1649
                  logfile, result.output)
1650

    
1651
    # If logfile is 'None' (during attach), it breaks TailFile
1652
    # TODO: have a log file for attach too
1653
    if action is not constants.ES_ACTION_ATTACH:
1654
      lines = [utils.SafeEncode(val)
1655
               for val in utils.TailFile(logfile, lines=20)]
1656
    else:
1657
      lines = result.output[-20:]
1658

    
1659
    base.ThrowError("External storage's %s script failed (%s), last"
1660
                    " lines of output:\n%s",
1661
                    action, result.fail_reason, "\n".join(lines))
1662

    
1663
  if action == constants.ES_ACTION_ATTACH:
1664
    return result.stdout
1665

    
1666

    
1667
def ExtStorageFromDisk(name, base_dir=None):
1668
  """Create an ExtStorage instance from disk.
1669

1670
  This function will return an ExtStorage instance
1671
  if the given name is a valid ExtStorage name.
1672

1673
  @type base_dir: string
1674
  @keyword base_dir: Base directory containing ExtStorage installations.
1675
                     Defaults to a search in all the ES_SEARCH_PATH dirs.
1676
  @rtype: tuple
1677
  @return: True and the ExtStorage instance if we find a valid one, or
1678
      False and the diagnose message on error
1679

1680
  """
1681
  if base_dir is None:
1682
    es_base_dir = pathutils.ES_SEARCH_PATH
1683
  else:
1684
    es_base_dir = [base_dir]
1685

    
1686
  es_dir = utils.FindFile(name, es_base_dir, os.path.isdir)
1687

    
1688
  if es_dir is None:
1689
    return False, ("Directory for External Storage Provider %s not"
1690
                   " found in search path" % name)
1691

    
1692
  # ES Files dictionary, we will populate it with the absolute path
1693
  # names; if the value is True, then it is a required file, otherwise
1694
  # an optional one
1695
  es_files = dict.fromkeys(constants.ES_SCRIPTS, True)
1696

    
1697
  es_files[constants.ES_PARAMETERS_FILE] = True
1698

    
1699
  for (filename, _) in es_files.items():
1700
    es_files[filename] = utils.PathJoin(es_dir, filename)
1701

    
1702
    try:
1703
      st = os.stat(es_files[filename])
1704
    except EnvironmentError, err:
1705
      return False, ("File '%s' under path '%s' is missing (%s)" %
1706
                     (filename, es_dir, utils.ErrnoOrStr(err)))
1707

    
1708
    if not stat.S_ISREG(stat.S_IFMT(st.st_mode)):
1709
      return False, ("File '%s' under path '%s' is not a regular file" %
1710
                     (filename, es_dir))
1711

    
1712
    if filename in constants.ES_SCRIPTS:
1713
      if stat.S_IMODE(st.st_mode) & stat.S_IXUSR != stat.S_IXUSR:
1714
        return False, ("File '%s' under path '%s' is not executable" %
1715
                       (filename, es_dir))
1716

    
1717
  parameters = []
1718
  if constants.ES_PARAMETERS_FILE in es_files:
1719
    parameters_file = es_files[constants.ES_PARAMETERS_FILE]
1720
    try:
1721
      parameters = utils.ReadFile(parameters_file).splitlines()
1722
    except EnvironmentError, err:
1723
      return False, ("Error while reading the EXT parameters file at %s: %s" %
1724
                     (parameters_file, utils.ErrnoOrStr(err)))
1725
    parameters = [v.split(None, 1) for v in parameters]
1726

    
1727
  es_obj = \
1728
    objects.ExtStorage(name=name, path=es_dir,
1729
                       create_script=es_files[constants.ES_SCRIPT_CREATE],
1730
                       remove_script=es_files[constants.ES_SCRIPT_REMOVE],
1731
                       grow_script=es_files[constants.ES_SCRIPT_GROW],
1732
                       attach_script=es_files[constants.ES_SCRIPT_ATTACH],
1733
                       detach_script=es_files[constants.ES_SCRIPT_DETACH],
1734
                       setinfo_script=es_files[constants.ES_SCRIPT_SETINFO],
1735
                       verify_script=es_files[constants.ES_SCRIPT_VERIFY],
1736
                       supported_parameters=parameters)
1737
  return True, es_obj
1738

    
1739

    
1740
def _ExtStorageEnvironment(unique_id, ext_params,
1741
                           size=None, grow=None, metadata=None):
1742
  """Calculate the environment for an External Storage script.
1743

1744
  @type unique_id: tuple (driver, vol_name)
1745
  @param unique_id: ExtStorage pool and name of the Volume
1746
  @type ext_params: dict
1747
  @param ext_params: the EXT parameters
1748
  @type size: string
1749
  @param size: size of the Volume (in mebibytes)
1750
  @type grow: string
1751
  @param grow: new size of Volume after grow (in mebibytes)
1752
  @type metadata: string
1753
  @param metadata: metadata info of the Volume
1754
  @rtype: dict
1755
  @return: dict of environment variables
1756

1757
  """
1758
  vol_name = unique_id[1]
1759

    
1760
  result = {}
1761
  result["VOL_NAME"] = vol_name
1762

    
1763
  # EXT params
1764
  for pname, pvalue in ext_params.items():
1765
    result["EXTP_%s" % pname.upper()] = str(pvalue)
1766

    
1767
  if size is not None:
1768
    result["VOL_SIZE"] = size
1769

    
1770
  if grow is not None:
1771
    result["VOL_NEW_SIZE"] = grow
1772

    
1773
  if metadata is not None:
1774
    result["VOL_METADATA"] = metadata
1775

    
1776
  return result
1777

    
1778

    
1779
def _VolumeLogName(kind, es_name, volume):
1780
  """Compute the ExtStorage log filename for a given Volume and operation.
1781

1782
  @type kind: string
1783
  @param kind: the operation type (e.g. create, remove etc.)
1784
  @type es_name: string
1785
  @param es_name: the ExtStorage name
1786
  @type volume: string
1787
  @param volume: the name of the Volume inside the External Storage
1788

1789
  """
1790
  # Check if the extstorage log dir is a valid dir
1791
  if not os.path.isdir(pathutils.LOG_ES_DIR):
1792
    base.ThrowError("Cannot find log directory: %s", pathutils.LOG_ES_DIR)
1793

    
1794
  # TODO: Use tempfile.mkstemp to create unique filename
1795
  basename = ("%s-%s-%s-%s.log" %
1796
              (kind, es_name, volume, utils.TimestampForFilename()))
1797
  return utils.PathJoin(pathutils.LOG_ES_DIR, basename)
1798

    
1799

    
1800
DEV_MAP = {
1801
  constants.LD_LV: LogicalVolume,
1802
  constants.LD_DRBD8: drbd.DRBD8Dev,
1803
  constants.LD_BLOCKDEV: PersistentBlockDevice,
1804
  constants.LD_RBD: RADOSBlockDevice,
1805
  constants.LD_EXT: ExtStorageDevice,
1806
  }
1807

    
1808
if constants.ENABLE_FILE_STORAGE or constants.ENABLE_SHARED_FILE_STORAGE:
1809
  DEV_MAP[constants.LD_FILE] = FileStorage
1810

    
1811

    
1812
def _VerifyDiskType(dev_type):
1813
  if dev_type not in DEV_MAP:
1814
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1815

    
1816

    
1817
def _VerifyDiskParams(disk):
1818
  """Verifies if all disk parameters are set.
1819

1820
  """
1821
  missing = set(constants.DISK_LD_DEFAULTS[disk.dev_type]) - set(disk.params)
1822
  if missing:
1823
    raise errors.ProgrammerError("Block device is missing disk parameters: %s" %
1824
                                 missing)
1825

    
1826

    
1827
def FindDevice(disk, children):
1828
  """Search for an existing, assembled device.
1829

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

1833
  @type disk: L{objects.Disk}
1834
  @param disk: the disk object to find
1835
  @type children: list of L{bdev.BlockDev}
1836
  @param children: the list of block devices that are children of the device
1837
                  represented by the disk parameter
1838

1839
  """
1840
  _VerifyDiskType(disk.dev_type)
1841
  device = DEV_MAP[disk.dev_type](disk.physical_id, children, disk.size,
1842
                                  disk.params)
1843
  if not device.attached:
1844
    return None
1845
  return device
1846

    
1847

    
1848
def Assemble(disk, children):
1849
  """Try to attach or assemble an existing device.
1850

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

1854
  @type disk: L{objects.Disk}
1855
  @param disk: the disk object to assemble
1856
  @type children: list of L{bdev.BlockDev}
1857
  @param children: the list of block devices that are children of the device
1858
                  represented by the disk parameter
1859

1860
  """
1861
  _VerifyDiskType(disk.dev_type)
1862
  _VerifyDiskParams(disk)
1863
  device = DEV_MAP[disk.dev_type](disk.physical_id, children, disk.size,
1864
                                  disk.params)
1865
  device.Assemble()
1866
  return device
1867

    
1868

    
1869
def Create(disk, children, excl_stor):
1870
  """Create a device.
1871

1872
  @type disk: L{objects.Disk}
1873
  @param disk: the disk object to create
1874
  @type children: list of L{bdev.BlockDev}
1875
  @param children: the list of block devices that are children of the device
1876
                  represented by the disk parameter
1877
  @type excl_stor: boolean
1878
  @param excl_stor: Whether exclusive_storage is active
1879
  @rtype: L{bdev.BlockDev}
1880
  @return: the created device, or C{None} in case of an error
1881

1882
  """
1883
  _VerifyDiskType(disk.dev_type)
1884
  _VerifyDiskParams(disk)
1885
  device = DEV_MAP[disk.dev_type].Create(disk.physical_id, children, disk.size,
1886
                                         disk.spindles, disk.params, excl_stor)
1887
  return device