Statistics
| Branch: | Tag: | Revision:

root / lib / storage / bdev.py @ 577edf04

History | View | Annotate | Download (57.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 _GetExclusiveStorageVgFree(cls, vg_name):
411
    """Return the free disk space in the given VG, in exclusive storage mode.
412

413
    @type vg_name: string
414
    @param vg_name: VG name
415
    @rtype: float
416
    @return: free space in MiB
417
    """
418
    pvs_info = cls.GetPVInfo([vg_name])
419
    if not pvs_info:
420
      return 0.0
421
    pv_size = cls._GetStdPvSize(pvs_info)
422
    num_pvs = len(cls._GetEmptyPvNames(pvs_info))
423
    return pv_size * num_pvs
424

    
425
  @classmethod
426
  def GetVGInfo(cls, vg_names, excl_stor, filter_readonly=True):
427
    """Get the free space info for specific VGs.
428

429
    @param vg_names: list of volume group names, if empty all will be returned
430
    @param excl_stor: whether exclusive_storage is enabled
431
    @param filter_readonly: whether to skip over readonly VGs
432

433
    @rtype: list
434
    @return: list of tuples (free_space, total_size, name) with free_space in
435
             MiB
436

437
    """
438
    try:
439
      info = cls._GetVolumeInfo("vgs", ["vg_name", "vg_free", "vg_attr",
440
                                        "vg_size"])
441
    except errors.GenericError, err:
442
      logging.error("Can't get VG information: %s", err)
443
      return None
444

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

    
460
    return data
461

    
462
  @classmethod
463
  def _ValidateName(cls, name):
464
    """Validates that a given name is valid as VG or LV name.
465

466
    The list of valid characters and restricted names is taken out of
467
    the lvm(8) manpage, with the simplification that we enforce both
468
    VG and LV restrictions on the names.
469

470
    """
471
    if (not cls._VALID_NAME_RE.match(name) or
472
        name in cls._INVALID_NAMES or
473
        compat.any(substring in name for substring in cls._INVALID_SUBSTRINGS)):
474
      base.ThrowError("Invalid LVM name '%s'", name)
475

    
476
  def Remove(self):
477
    """Remove this logical volume.
478

479
    """
480
    if not self.minor and not self.Attach():
481
      # the LV does not exist
482
      return
483
    result = utils.RunCmd(["lvremove", "-f", "%s/%s" %
484
                           (self._vg_name, self._lv_name)])
485
    if result.failed:
486
      base.ThrowError("Can't lvremove: %s - %s",
487
                      result.fail_reason, result.output)
488

    
489
  def Rename(self, new_id):
490
    """Rename this logical volume.
491

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

    
506
  @classmethod
507
  def _ParseLvInfoLine(cls, line, sep):
508
    """Parse one line of the lvs output used in L{_GetLvInfo}.
509

510
    """
511
    elems = line.strip().rstrip(sep).split(sep)
512
    if len(elems) != 6:
513
      base.ThrowError("Can't parse LVS output, len(%s) != 6", str(elems))
514

    
515
    (status, major, minor, pe_size, stripes, pvs) = elems
516
    if len(status) < 6:
517
      base.ThrowError("lvs lv_attr is not at least 6 characters (%s)", status)
518

    
519
    try:
520
      major = int(major)
521
      minor = int(minor)
522
    except (TypeError, ValueError), err:
523
      base.ThrowError("lvs major/minor cannot be parsed: %s", str(err))
524

    
525
    try:
526
      pe_size = int(float(pe_size))
527
    except (TypeError, ValueError), err:
528
      base.ThrowError("Can't parse vg extent size: %s", err)
529

    
530
    try:
531
      stripes = int(stripes)
532
    except (TypeError, ValueError), err:
533
      base.ThrowError("Can't parse the number of stripes: %s", err)
534

    
535
    pv_names = []
536
    for pv in pvs.split(","):
537
      m = re.match(cls._PARSE_PV_DEV_RE, pv)
538
      if not m:
539
        base.ThrowError("Can't parse this device list: %s", pvs)
540
      pv_names.append(m.group(1))
541
    assert len(pv_names) > 0
542

    
543
    return (status, major, minor, pe_size, stripes, pv_names)
544

    
545
  @classmethod
546
  def _GetLvInfo(cls, dev_path, _run_cmd=utils.RunCmd):
547
    """Get info about the given existing LV to be used.
548

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

    
574
  def Attach(self):
575
    """Attach to an existing LV.
576

577
    This method will try to see if an existing and active LV exists
578
    which matches our name. If so, its major/minor will be
579
    recorded.
580

581
    """
582
    self.attached = False
583
    try:
584
      (status, major, minor, pe_size, stripes, pv_names) = \
585
        self._GetLvInfo(self.dev_path)
586
    except errors.BlockDeviceError:
587
      return False
588

    
589
    self.major = major
590
    self.minor = minor
591
    self.pe_size = pe_size
592
    self.stripe_count = stripes
593
    self._degraded = status[0] == "v" # virtual volume, i.e. doesn't backing
594
                                      # storage
595
    self.pv_names = pv_names
596
    self.attached = True
597
    return True
598

    
599
  def Assemble(self):
600
    """Assemble the device.
601

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

606
    """
607
    result = utils.RunCmd(["lvchange", "-ay", self.dev_path])
608
    if result.failed:
609
      base.ThrowError("Can't activate lv %s: %s", self.dev_path, result.output)
610

    
611
  def Shutdown(self):
612
    """Shutdown the device.
613

614
    This is a no-op for the LV device type, as we don't deactivate the
615
    volumes on shutdown.
616

617
    """
618
    pass
619

    
620
  def GetSyncStatus(self):
621
    """Returns the sync status of the device.
622

623
    If this device is a mirroring device, this function returns the
624
    status of the mirror.
625

626
    For logical volumes, sync_percent and estimated_time are always
627
    None (no recovery in progress, as we don't handle the mirrored LV
628
    case). The is_degraded parameter is the inverse of the ldisk
629
    parameter.
630

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

637
    The status was already read in Attach, so we just return it.
638

639
    @rtype: objects.BlockDevStatus
640

641
    """
642
    if self._degraded:
643
      ldisk_status = constants.LDS_FAULTY
644
    else:
645
      ldisk_status = constants.LDS_OKAY
646

    
647
    return objects.BlockDevStatus(dev_path=self.dev_path,
648
                                  major=self.major,
649
                                  minor=self.minor,
650
                                  sync_percent=None,
651
                                  estimated_time=None,
652
                                  is_degraded=self._degraded,
653
                                  ldisk_status=ldisk_status)
654

    
655
  def Open(self, force=False):
656
    """Make the device ready for I/O.
657

658
    This is a no-op for the LV device type.
659

660
    """
661
    pass
662

    
663
  def Close(self):
664
    """Notifies that the device will no longer be used for I/O.
665

666
    This is a no-op for the LV device type.
667

668
    """
669
    pass
670

    
671
  def Snapshot(self, size):
672
    """Create a snapshot copy of an lvm block device.
673

674
    @returns: tuple (vg, lv)
675

676
    """
677
    snap_name = self._lv_name + ".snap"
678

    
679
    # remove existing snapshot if found
680
    snap = LogicalVolume((self._vg_name, snap_name), None, size, self.params)
681
    base.IgnoreError(snap.Remove)
682

    
683
    vg_info = self.GetVGInfo([self._vg_name], False)
684
    if not vg_info:
685
      base.ThrowError("Can't compute VG info for vg %s", self._vg_name)
686
    free_size, _, _ = vg_info[0]
687
    if free_size < size:
688
      base.ThrowError("Not enough free space: required %s,"
689
                      " available %s", size, free_size)
690

    
691
    _CheckResult(utils.RunCmd(["lvcreate", "-L%dm" % size, "-s",
692
                               "-n%s" % snap_name, self.dev_path]))
693

    
694
    return (self._vg_name, snap_name)
695

    
696
  def _RemoveOldInfo(self):
697
    """Try to remove old tags from the lv.
698

699
    """
700
    result = utils.RunCmd(["lvs", "-o", "tags", "--noheadings", "--nosuffix",
701
                           self.dev_path])
702
    _CheckResult(result)
703

    
704
    raw_tags = result.stdout.strip()
705
    if raw_tags:
706
      for tag in raw_tags.split(","):
707
        _CheckResult(utils.RunCmd(["lvchange", "--deltag",
708
                                   tag.strip(), self.dev_path]))
709

    
710
  def SetInfo(self, text):
711
    """Update metadata with info text.
712

713
    """
714
    base.BlockDev.SetInfo(self, text)
715

    
716
    self._RemoveOldInfo()
717

    
718
    # Replace invalid characters
719
    text = re.sub("^[^A-Za-z0-9_+.]", "_", text)
720
    text = re.sub("[^-A-Za-z0-9_+.]", "_", text)
721

    
722
    # Only up to 128 characters are allowed
723
    text = text[:128]
724

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

    
727
  def Grow(self, amount, dryrun, backingstore):
728
    """Grow the logical volume.
729

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

    
755
  def GetActualSpindles(self):
756
    """Return the number of spindles used.
757

758
    """
759
    assert self.attached, "BlockDevice not attached in GetActualSpindles()"
760
    return len(self.pv_names)
761

    
762

    
763
class FileStorage(base.BlockDev):
764
  """File device.
765

766
  This class represents the a file storage backend device.
767

768
  The unique_id for the file device is a (file_driver, file_path) tuple.
769

770
  """
771
  def __init__(self, unique_id, children, size, params):
772
    """Initalizes a file device backend.
773

774
    """
775
    if children:
776
      raise errors.BlockDeviceError("Invalid setup for file device")
777
    super(FileStorage, self).__init__(unique_id, children, size, params)
778
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
779
      raise ValueError("Invalid configuration data %s" % str(unique_id))
780
    self.driver = unique_id[0]
781
    self.dev_path = unique_id[1]
782

    
783
    CheckFileStoragePath(self.dev_path)
784

    
785
    self.Attach()
786

    
787
  def Assemble(self):
788
    """Assemble the device.
789

790
    Checks whether the file device exists, raises BlockDeviceError otherwise.
791

792
    """
793
    if not os.path.exists(self.dev_path):
794
      base.ThrowError("File device '%s' does not exist" % self.dev_path)
795

    
796
  def Shutdown(self):
797
    """Shutdown the device.
798

799
    This is a no-op for the file type, as we don't deactivate
800
    the file on shutdown.
801

802
    """
803
    pass
804

    
805
  def Open(self, force=False):
806
    """Make the device ready for I/O.
807

808
    This is a no-op for the file type.
809

810
    """
811
    pass
812

    
813
  def Close(self):
814
    """Notifies that the device will no longer be used for I/O.
815

816
    This is a no-op for the file type.
817

818
    """
819
    pass
820

    
821
  def Remove(self):
822
    """Remove the file backing the block device.
823

824
    @rtype: boolean
825
    @return: True if the removal was successful
826

827
    """
828
    try:
829
      os.remove(self.dev_path)
830
    except OSError, err:
831
      if err.errno != errno.ENOENT:
832
        base.ThrowError("Can't remove file '%s': %s", self.dev_path, err)
833

    
834
  def Rename(self, new_id):
835
    """Renames the file.
836

837
    """
838
    # TODO: implement rename for file-based storage
839
    base.ThrowError("Rename is not supported for file-based storage")
840

    
841
  def Grow(self, amount, dryrun, backingstore):
842
    """Grow the file
843

844
    @param amount: the amount (in mebibytes) to grow with
845

846
    """
847
    if not backingstore:
848
      return
849
    # Check that the file exists
850
    self.Assemble()
851
    current_size = self.GetActualSize()
852
    new_size = current_size + amount * 1024 * 1024
853
    assert new_size > current_size, "Cannot Grow with a negative amount"
854
    # We can't really simulate the growth
855
    if dryrun:
856
      return
857
    try:
858
      f = open(self.dev_path, "a+")
859
      f.truncate(new_size)
860
      f.close()
861
    except EnvironmentError, err:
862
      base.ThrowError("Error in file growth: %", str(err))
863

    
864
  def Attach(self):
865
    """Attach to an existing file.
866

867
    Check if this file already exists.
868

869
    @rtype: boolean
870
    @return: True if file exists
871

872
    """
873
    self.attached = os.path.exists(self.dev_path)
874
    return self.attached
875

    
876
  def GetActualSize(self):
877
    """Return the actual disk size.
878

879
    @note: the device needs to be active when this is called
880

881
    """
882
    assert self.attached, "BlockDevice not attached in GetActualSize()"
883
    try:
884
      st = os.stat(self.dev_path)
885
      return st.st_size
886
    except OSError, err:
887
      base.ThrowError("Can't stat %s: %s", self.dev_path, err)
888

    
889
  @classmethod
890
  def Create(cls, unique_id, children, size, spindles, params, excl_stor):
891
    """Create a new file.
892

893
    @param size: the size of file in MiB
894

895
    @rtype: L{bdev.FileStorage}
896
    @return: an instance of FileStorage
897

898
    """
899
    if excl_stor:
900
      raise errors.ProgrammerError("FileStorage device requested with"
901
                                   " exclusive_storage")
902
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
903
      raise ValueError("Invalid configuration data %s" % str(unique_id))
904

    
905
    dev_path = unique_id[1]
906

    
907
    CheckFileStoragePath(dev_path)
908

    
909
    try:
910
      fd = os.open(dev_path, os.O_RDWR | os.O_CREAT | os.O_EXCL)
911
      f = os.fdopen(fd, "w")
912
      f.truncate(size * 1024 * 1024)
913
      f.close()
914
    except EnvironmentError, err:
915
      if err.errno == errno.EEXIST:
916
        base.ThrowError("File already existing: %s", dev_path)
917
      base.ThrowError("Error in file creation: %", str(err))
918

    
919
    return FileStorage(unique_id, children, size, params)
920

    
921

    
922
class PersistentBlockDevice(base.BlockDev):
923
  """A block device with persistent node
924

925
  May be either directly attached, or exposed through DM (e.g. dm-multipath).
926
  udev helpers are probably required to give persistent, human-friendly
927
  names.
928

929
  For the time being, pathnames are required to lie under /dev.
930

931
  """
932
  def __init__(self, unique_id, children, size, params):
933
    """Attaches to a static block device.
934

935
    The unique_id is a path under /dev.
936

937
    """
938
    super(PersistentBlockDevice, self).__init__(unique_id, children, size,
939
                                                params)
940
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
941
      raise ValueError("Invalid configuration data %s" % str(unique_id))
942
    self.dev_path = unique_id[1]
943
    if not os.path.realpath(self.dev_path).startswith("/dev/"):
944
      raise ValueError("Full path '%s' lies outside /dev" %
945
                              os.path.realpath(self.dev_path))
946
    # TODO: this is just a safety guard checking that we only deal with devices
947
    # we know how to handle. In the future this will be integrated with
948
    # external storage backends and possible values will probably be collected
949
    # from the cluster configuration.
950
    if unique_id[0] != constants.BLOCKDEV_DRIVER_MANUAL:
951
      raise ValueError("Got persistent block device of invalid type: %s" %
952
                       unique_id[0])
953

    
954
    self.major = self.minor = None
955
    self.Attach()
956

    
957
  @classmethod
958
  def Create(cls, unique_id, children, size, spindles, params, excl_stor):
959
    """Create a new device
960

961
    This is a noop, we only return a PersistentBlockDevice instance
962

963
    """
964
    if excl_stor:
965
      raise errors.ProgrammerError("Persistent block device requested with"
966
                                   " exclusive_storage")
967
    return PersistentBlockDevice(unique_id, children, 0, params)
968

    
969
  def Remove(self):
970
    """Remove a device
971

972
    This is a noop
973

974
    """
975
    pass
976

    
977
  def Rename(self, new_id):
978
    """Rename this device.
979

980
    """
981
    base.ThrowError("Rename is not supported for PersistentBlockDev storage")
982

    
983
  def Attach(self):
984
    """Attach to an existing block device.
985

986

987
    """
988
    self.attached = False
989
    try:
990
      st = os.stat(self.dev_path)
991
    except OSError, err:
992
      logging.error("Error stat()'ing %s: %s", self.dev_path, str(err))
993
      return False
994

    
995
    if not stat.S_ISBLK(st.st_mode):
996
      logging.error("%s is not a block device", self.dev_path)
997
      return False
998

    
999
    self.major = os.major(st.st_rdev)
1000
    self.minor = os.minor(st.st_rdev)
1001
    self.attached = True
1002

    
1003
    return True
1004

    
1005
  def Assemble(self):
1006
    """Assemble the device.
1007

1008
    """
1009
    pass
1010

    
1011
  def Shutdown(self):
1012
    """Shutdown the device.
1013

1014
    """
1015
    pass
1016

    
1017
  def Open(self, force=False):
1018
    """Make the device ready for I/O.
1019

1020
    """
1021
    pass
1022

    
1023
  def Close(self):
1024
    """Notifies that the device will no longer be used for I/O.
1025

1026
    """
1027
    pass
1028

    
1029
  def Grow(self, amount, dryrun, backingstore):
1030
    """Grow the logical volume.
1031

1032
    """
1033
    base.ThrowError("Grow is not supported for PersistentBlockDev storage")
1034

    
1035

    
1036
class RADOSBlockDevice(base.BlockDev):
1037
  """A RADOS Block Device (rbd).
1038

1039
  This class implements the RADOS Block Device for the backend. You need
1040
  the rbd kernel driver, the RADOS Tools and a working RADOS cluster for
1041
  this to be functional.
1042

1043
  """
1044
  def __init__(self, unique_id, children, size, params):
1045
    """Attaches to an rbd device.
1046

1047
    """
1048
    super(RADOSBlockDevice, self).__init__(unique_id, children, size, params)
1049
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1050
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1051

    
1052
    self.driver, self.rbd_name = unique_id
1053

    
1054
    self.major = self.minor = None
1055
    self.Attach()
1056

    
1057
  @classmethod
1058
  def Create(cls, unique_id, children, size, spindles, params, excl_stor):
1059
    """Create a new rbd device.
1060

1061
    Provision a new rbd volume inside a RADOS pool.
1062

1063
    """
1064
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1065
      raise errors.ProgrammerError("Invalid configuration data %s" %
1066
                                   str(unique_id))
1067
    if excl_stor:
1068
      raise errors.ProgrammerError("RBD device requested with"
1069
                                   " exclusive_storage")
1070
    rbd_pool = params[constants.LDP_POOL]
1071
    rbd_name = unique_id[1]
1072

    
1073
    # Provision a new rbd volume (Image) inside the RADOS cluster.
1074
    cmd = [constants.RBD_CMD, "create", "-p", rbd_pool,
1075
           rbd_name, "--size", "%s" % size]
1076
    result = utils.RunCmd(cmd)
1077
    if result.failed:
1078
      base.ThrowError("rbd creation failed (%s): %s",
1079
                      result.fail_reason, result.output)
1080

    
1081
    return RADOSBlockDevice(unique_id, children, size, params)
1082

    
1083
  def Remove(self):
1084
    """Remove the rbd device.
1085

1086
    """
1087
    rbd_pool = self.params[constants.LDP_POOL]
1088
    rbd_name = self.unique_id[1]
1089

    
1090
    if not self.minor and not self.Attach():
1091
      # The rbd device doesn't exist.
1092
      return
1093

    
1094
    # First shutdown the device (remove mappings).
1095
    self.Shutdown()
1096

    
1097
    # Remove the actual Volume (Image) from the RADOS cluster.
1098
    cmd = [constants.RBD_CMD, "rm", "-p", rbd_pool, rbd_name]
1099
    result = utils.RunCmd(cmd)
1100
    if result.failed:
1101
      base.ThrowError("Can't remove Volume from cluster with rbd rm: %s - %s",
1102
                      result.fail_reason, result.output)
1103

    
1104
  def Rename(self, new_id):
1105
    """Rename this device.
1106

1107
    """
1108
    pass
1109

    
1110
  def Attach(self):
1111
    """Attach to an existing rbd device.
1112

1113
    This method maps the rbd volume that matches our name with
1114
    an rbd device and then attaches to this device.
1115

1116
    """
1117
    self.attached = False
1118

    
1119
    # Map the rbd volume to a block device under /dev
1120
    self.dev_path = self._MapVolumeToBlockdev(self.unique_id)
1121

    
1122
    try:
1123
      st = os.stat(self.dev_path)
1124
    except OSError, err:
1125
      logging.error("Error stat()'ing %s: %s", self.dev_path, str(err))
1126
      return False
1127

    
1128
    if not stat.S_ISBLK(st.st_mode):
1129
      logging.error("%s is not a block device", self.dev_path)
1130
      return False
1131

    
1132
    self.major = os.major(st.st_rdev)
1133
    self.minor = os.minor(st.st_rdev)
1134
    self.attached = True
1135

    
1136
    return True
1137

    
1138
  def _MapVolumeToBlockdev(self, unique_id):
1139
    """Maps existing rbd volumes to block devices.
1140

1141
    This method should be idempotent if the mapping already exists.
1142

1143
    @rtype: string
1144
    @return: the block device path that corresponds to the volume
1145

1146
    """
1147
    pool = self.params[constants.LDP_POOL]
1148
    name = unique_id[1]
1149

    
1150
    # Check if the mapping already exists.
1151
    rbd_dev = self._VolumeToBlockdev(pool, name)
1152
    if rbd_dev:
1153
      # The mapping exists. Return it.
1154
      return rbd_dev
1155

    
1156
    # The mapping doesn't exist. Create it.
1157
    map_cmd = [constants.RBD_CMD, "map", "-p", pool, name]
1158
    result = utils.RunCmd(map_cmd)
1159
    if result.failed:
1160
      base.ThrowError("rbd map failed (%s): %s",
1161
                      result.fail_reason, result.output)
1162

    
1163
    # Find the corresponding rbd device.
1164
    rbd_dev = self._VolumeToBlockdev(pool, name)
1165
    if not rbd_dev:
1166
      base.ThrowError("rbd map succeeded, but could not find the rbd block"
1167
                      " device in output of showmapped, for volume: %s", name)
1168

    
1169
    # The device was successfully mapped. Return it.
1170
    return rbd_dev
1171

    
1172
  @classmethod
1173
  def _VolumeToBlockdev(cls, pool, volume_name):
1174
    """Do the 'volume name'-to-'rbd block device' resolving.
1175

1176
    @type pool: string
1177
    @param pool: RADOS pool to use
1178
    @type volume_name: string
1179
    @param volume_name: the name of the volume whose device we search for
1180
    @rtype: string or None
1181
    @return: block device path if the volume is mapped, else None
1182

1183
    """
1184
    try:
1185
      # Newer versions of the rbd tool support json output formatting. Use it
1186
      # if available.
1187
      showmap_cmd = [
1188
        constants.RBD_CMD,
1189
        "showmapped",
1190
        "-p",
1191
        pool,
1192
        "--format",
1193
        "json"
1194
        ]
1195
      result = utils.RunCmd(showmap_cmd)
1196
      if result.failed:
1197
        logging.error("rbd JSON output formatting returned error (%s): %s,"
1198
                      "falling back to plain output parsing",
1199
                      result.fail_reason, result.output)
1200
        raise RbdShowmappedJsonError
1201

    
1202
      return cls._ParseRbdShowmappedJson(result.output, volume_name)
1203
    except RbdShowmappedJsonError:
1204
      # For older versions of rbd, we have to parse the plain / text output
1205
      # manually.
1206
      showmap_cmd = [constants.RBD_CMD, "showmapped", "-p", pool]
1207
      result = utils.RunCmd(showmap_cmd)
1208
      if result.failed:
1209
        base.ThrowError("rbd showmapped failed (%s): %s",
1210
                        result.fail_reason, result.output)
1211

    
1212
      return cls._ParseRbdShowmappedPlain(result.output, volume_name)
1213

    
1214
  @staticmethod
1215
  def _ParseRbdShowmappedJson(output, volume_name):
1216
    """Parse the json output of `rbd showmapped'.
1217

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

1221
    @type output: string
1222
    @param output: the json output of `rbd showmapped'
1223
    @type volume_name: string
1224
    @param volume_name: the name of the volume whose device we search for
1225
    @rtype: string or None
1226
    @return: block device path if the volume is mapped, else None
1227

1228
    """
1229
    try:
1230
      devices = serializer.LoadJson(output)
1231
    except ValueError, err:
1232
      base.ThrowError("Unable to parse JSON data: %s" % err)
1233

    
1234
    rbd_dev = None
1235
    for d in devices.values(): # pylint: disable=E1103
1236
      try:
1237
        name = d["name"]
1238
      except KeyError:
1239
        base.ThrowError("'name' key missing from json object %s", devices)
1240

    
1241
      if name == volume_name:
1242
        if rbd_dev is not None:
1243
          base.ThrowError("rbd volume %s is mapped more than once", volume_name)
1244

    
1245
        rbd_dev = d["device"]
1246

    
1247
    return rbd_dev
1248

    
1249
  @staticmethod
1250
  def _ParseRbdShowmappedPlain(output, volume_name):
1251
    """Parse the (plain / text) output of `rbd showmapped'.
1252

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

1257
    @type output: string
1258
    @param output: the plain text 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
    allfields = 5
1266
    volumefield = 2
1267
    devicefield = 4
1268

    
1269
    lines = output.splitlines()
1270

    
1271
    # Try parsing the new output format (ceph >= 0.55).
1272
    splitted_lines = map(lambda l: l.split(), lines)
1273

    
1274
    # Check for empty output.
1275
    if not splitted_lines:
1276
      return None
1277

    
1278
    # Check showmapped output, to determine number of fields.
1279
    field_cnt = len(splitted_lines[0])
1280
    if field_cnt != allfields:
1281
      # Parsing the new format failed. Fallback to parsing the old output
1282
      # format (< 0.55).
1283
      splitted_lines = map(lambda l: l.split("\t"), lines)
1284
      if field_cnt != allfields:
1285
        base.ThrowError("Cannot parse rbd showmapped output expected %s fields,"
1286
                        " found %s", allfields, field_cnt)
1287

    
1288
    matched_lines = \
1289
      filter(lambda l: len(l) == allfields and l[volumefield] == volume_name,
1290
             splitted_lines)
1291

    
1292
    if len(matched_lines) > 1:
1293
      base.ThrowError("rbd volume %s mapped more than once", volume_name)
1294

    
1295
    if matched_lines:
1296
      # rbd block device found. Return it.
1297
      rbd_dev = matched_lines[0][devicefield]
1298
      return rbd_dev
1299

    
1300
    # The given volume is not mapped.
1301
    return None
1302

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

1306
    """
1307
    pass
1308

    
1309
  def Shutdown(self):
1310
    """Shutdown the device.
1311

1312
    """
1313
    if not self.minor and not self.Attach():
1314
      # The rbd device doesn't exist.
1315
      return
1316

    
1317
    # Unmap the block device from the Volume.
1318
    self._UnmapVolumeFromBlockdev(self.unique_id)
1319

    
1320
    self.minor = None
1321
    self.dev_path = None
1322

    
1323
  def _UnmapVolumeFromBlockdev(self, unique_id):
1324
    """Unmaps the rbd device from the Volume it is mapped.
1325

1326
    Unmaps the rbd device from the Volume it was previously mapped to.
1327
    This method should be idempotent if the Volume isn't mapped.
1328

1329
    """
1330
    pool = self.params[constants.LDP_POOL]
1331
    name = unique_id[1]
1332

    
1333
    # Check if the mapping already exists.
1334
    rbd_dev = self._VolumeToBlockdev(pool, name)
1335

    
1336
    if rbd_dev:
1337
      # The mapping exists. Unmap the rbd device.
1338
      unmap_cmd = [constants.RBD_CMD, "unmap", "%s" % rbd_dev]
1339
      result = utils.RunCmd(unmap_cmd)
1340
      if result.failed:
1341
        base.ThrowError("rbd unmap failed (%s): %s",
1342
                        result.fail_reason, result.output)
1343

    
1344
  def Open(self, force=False):
1345
    """Make the device ready for I/O.
1346

1347
    """
1348
    pass
1349

    
1350
  def Close(self):
1351
    """Notifies that the device will no longer be used for I/O.
1352

1353
    """
1354
    pass
1355

    
1356
  def Grow(self, amount, dryrun, backingstore):
1357
    """Grow the Volume.
1358

1359
    @type amount: integer
1360
    @param amount: the amount (in mebibytes) to grow with
1361
    @type dryrun: boolean
1362
    @param dryrun: whether to execute the operation in simulation mode
1363
        only, without actually increasing the size
1364

1365
    """
1366
    if not backingstore:
1367
      return
1368
    if not self.Attach():
1369
      base.ThrowError("Can't attach to rbd device during Grow()")
1370

    
1371
    if dryrun:
1372
      # the rbd tool does not support dry runs of resize operations.
1373
      # Since rbd volumes are thinly provisioned, we assume
1374
      # there is always enough free space for the operation.
1375
      return
1376

    
1377
    rbd_pool = self.params[constants.LDP_POOL]
1378
    rbd_name = self.unique_id[1]
1379
    new_size = self.size + amount
1380

    
1381
    # Resize the rbd volume (Image) inside the RADOS cluster.
1382
    cmd = [constants.RBD_CMD, "resize", "-p", rbd_pool,
1383
           rbd_name, "--size", "%s" % new_size]
1384
    result = utils.RunCmd(cmd)
1385
    if result.failed:
1386
      base.ThrowError("rbd resize failed (%s): %s",
1387
                      result.fail_reason, result.output)
1388

    
1389

    
1390
class ExtStorageDevice(base.BlockDev):
1391
  """A block device provided by an ExtStorage Provider.
1392

1393
  This class implements the External Storage Interface, which means
1394
  handling of the externally provided block devices.
1395

1396
  """
1397
  def __init__(self, unique_id, children, size, params):
1398
    """Attaches to an extstorage block device.
1399

1400
    """
1401
    super(ExtStorageDevice, self).__init__(unique_id, children, size, params)
1402
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1403
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1404

    
1405
    self.driver, self.vol_name = unique_id
1406
    self.ext_params = params
1407

    
1408
    self.major = self.minor = None
1409
    self.Attach()
1410

    
1411
  @classmethod
1412
  def Create(cls, unique_id, children, size, spindles, params, excl_stor):
1413
    """Create a new extstorage device.
1414

1415
    Provision a new volume using an extstorage provider, which will
1416
    then be mapped to a block device.
1417

1418
    """
1419
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1420
      raise errors.ProgrammerError("Invalid configuration data %s" %
1421
                                   str(unique_id))
1422
    if excl_stor:
1423
      raise errors.ProgrammerError("extstorage device requested with"
1424
                                   " exclusive_storage")
1425

    
1426
    # Call the External Storage's create script,
1427
    # to provision a new Volume inside the External Storage
1428
    _ExtStorageAction(constants.ES_ACTION_CREATE, unique_id,
1429
                      params, str(size))
1430

    
1431
    return ExtStorageDevice(unique_id, children, size, params)
1432

    
1433
  def Remove(self):
1434
    """Remove the extstorage device.
1435

1436
    """
1437
    if not self.minor and not self.Attach():
1438
      # The extstorage device doesn't exist.
1439
      return
1440

    
1441
    # First shutdown the device (remove mappings).
1442
    self.Shutdown()
1443

    
1444
    # Call the External Storage's remove script,
1445
    # to remove the Volume from the External Storage
1446
    _ExtStorageAction(constants.ES_ACTION_REMOVE, self.unique_id,
1447
                      self.ext_params)
1448

    
1449
  def Rename(self, new_id):
1450
    """Rename this device.
1451

1452
    """
1453
    pass
1454

    
1455
  def Attach(self):
1456
    """Attach to an existing extstorage device.
1457

1458
    This method maps the extstorage volume that matches our name with
1459
    a corresponding block device and then attaches to this device.
1460

1461
    """
1462
    self.attached = False
1463

    
1464
    # Call the External Storage's attach script,
1465
    # to attach an existing Volume to a block device under /dev
1466
    self.dev_path = _ExtStorageAction(constants.ES_ACTION_ATTACH,
1467
                                      self.unique_id, self.ext_params)
1468

    
1469
    try:
1470
      st = os.stat(self.dev_path)
1471
    except OSError, err:
1472
      logging.error("Error stat()'ing %s: %s", self.dev_path, str(err))
1473
      return False
1474

    
1475
    if not stat.S_ISBLK(st.st_mode):
1476
      logging.error("%s is not a block device", self.dev_path)
1477
      return False
1478

    
1479
    self.major = os.major(st.st_rdev)
1480
    self.minor = os.minor(st.st_rdev)
1481
    self.attached = True
1482

    
1483
    return True
1484

    
1485
  def Assemble(self):
1486
    """Assemble the device.
1487

1488
    """
1489
    pass
1490

    
1491
  def Shutdown(self):
1492
    """Shutdown the device.
1493

1494
    """
1495
    if not self.minor and not self.Attach():
1496
      # The extstorage device doesn't exist.
1497
      return
1498

    
1499
    # Call the External Storage's detach script,
1500
    # to detach an existing Volume from it's block device under /dev
1501
    _ExtStorageAction(constants.ES_ACTION_DETACH, self.unique_id,
1502
                      self.ext_params)
1503

    
1504
    self.minor = None
1505
    self.dev_path = None
1506

    
1507
  def Open(self, force=False):
1508
    """Make the device ready for I/O.
1509

1510
    """
1511
    pass
1512

    
1513
  def Close(self):
1514
    """Notifies that the device will no longer be used for I/O.
1515

1516
    """
1517
    pass
1518

    
1519
  def Grow(self, amount, dryrun, backingstore):
1520
    """Grow the Volume.
1521

1522
    @type amount: integer
1523
    @param amount: the amount (in mebibytes) to grow with
1524
    @type dryrun: boolean
1525
    @param dryrun: whether to execute the operation in simulation mode
1526
        only, without actually increasing the size
1527

1528
    """
1529
    if not backingstore:
1530
      return
1531
    if not self.Attach():
1532
      base.ThrowError("Can't attach to extstorage device during Grow()")
1533

    
1534
    if dryrun:
1535
      # we do not support dry runs of resize operations for now.
1536
      return
1537

    
1538
    new_size = self.size + amount
1539

    
1540
    # Call the External Storage's grow script,
1541
    # to grow an existing Volume inside the External Storage
1542
    _ExtStorageAction(constants.ES_ACTION_GROW, self.unique_id,
1543
                      self.ext_params, str(self.size), grow=str(new_size))
1544

    
1545
  def SetInfo(self, text):
1546
    """Update metadata with info text.
1547

1548
    """
1549
    # Replace invalid characters
1550
    text = re.sub("^[^A-Za-z0-9_+.]", "_", text)
1551
    text = re.sub("[^-A-Za-z0-9_+.]", "_", text)
1552

    
1553
    # Only up to 128 characters are allowed
1554
    text = text[:128]
1555

    
1556
    # Call the External Storage's setinfo script,
1557
    # to set metadata for an existing Volume inside the External Storage
1558
    _ExtStorageAction(constants.ES_ACTION_SETINFO, self.unique_id,
1559
                      self.ext_params, metadata=text)
1560

    
1561

    
1562
def _ExtStorageAction(action, unique_id, ext_params,
1563
                      size=None, grow=None, metadata=None):
1564
  """Take an External Storage action.
1565

1566
  Take an External Storage action concerning or affecting
1567
  a specific Volume inside the External Storage.
1568

1569
  @type action: string
1570
  @param action: which action to perform. One of:
1571
                 create / remove / grow / attach / detach
1572
  @type unique_id: tuple (driver, vol_name)
1573
  @param unique_id: a tuple containing the type of ExtStorage (driver)
1574
                    and the Volume name
1575
  @type ext_params: dict
1576
  @param ext_params: ExtStorage parameters
1577
  @type size: integer
1578
  @param size: the size of the Volume in mebibytes
1579
  @type grow: integer
1580
  @param grow: the new size in mebibytes (after grow)
1581
  @type metadata: string
1582
  @param metadata: metadata info of the Volume, for use by the provider
1583
  @rtype: None or a block device path (during attach)
1584

1585
  """
1586
  driver, vol_name = unique_id
1587

    
1588
  # Create an External Storage instance of type `driver'
1589
  status, inst_es = ExtStorageFromDisk(driver)
1590
  if not status:
1591
    base.ThrowError("%s" % inst_es)
1592

    
1593
  # Create the basic environment for the driver's scripts
1594
  create_env = _ExtStorageEnvironment(unique_id, ext_params, size,
1595
                                      grow, metadata)
1596

    
1597
  # Do not use log file for action `attach' as we need
1598
  # to get the output from RunResult
1599
  # TODO: find a way to have a log file for attach too
1600
  logfile = None
1601
  if action is not constants.ES_ACTION_ATTACH:
1602
    logfile = _VolumeLogName(action, driver, vol_name)
1603

    
1604
  # Make sure the given action results in a valid script
1605
  if action not in constants.ES_SCRIPTS:
1606
    base.ThrowError("Action '%s' doesn't result in a valid ExtStorage script" %
1607
                    action)
1608

    
1609
  # Find out which external script to run according the given action
1610
  script_name = action + "_script"
1611
  script = getattr(inst_es, script_name)
1612

    
1613
  # Run the external script
1614
  result = utils.RunCmd([script], env=create_env,
1615
                        cwd=inst_es.path, output=logfile,)
1616
  if result.failed:
1617
    logging.error("External storage's %s command '%s' returned"
1618
                  " error: %s, logfile: %s, output: %s",
1619
                  action, result.cmd, result.fail_reason,
1620
                  logfile, result.output)
1621

    
1622
    # If logfile is 'None' (during attach), it breaks TailFile
1623
    # TODO: have a log file for attach too
1624
    if action is not constants.ES_ACTION_ATTACH:
1625
      lines = [utils.SafeEncode(val)
1626
               for val in utils.TailFile(logfile, lines=20)]
1627
    else:
1628
      lines = result.output[-20:]
1629

    
1630
    base.ThrowError("External storage's %s script failed (%s), last"
1631
                    " lines of output:\n%s",
1632
                    action, result.fail_reason, "\n".join(lines))
1633

    
1634
  if action == constants.ES_ACTION_ATTACH:
1635
    return result.stdout
1636

    
1637

    
1638
def ExtStorageFromDisk(name, base_dir=None):
1639
  """Create an ExtStorage instance from disk.
1640

1641
  This function will return an ExtStorage instance
1642
  if the given name is a valid ExtStorage name.
1643

1644
  @type base_dir: string
1645
  @keyword base_dir: Base directory containing ExtStorage installations.
1646
                     Defaults to a search in all the ES_SEARCH_PATH dirs.
1647
  @rtype: tuple
1648
  @return: True and the ExtStorage instance if we find a valid one, or
1649
      False and the diagnose message on error
1650

1651
  """
1652
  if base_dir is None:
1653
    es_base_dir = pathutils.ES_SEARCH_PATH
1654
  else:
1655
    es_base_dir = [base_dir]
1656

    
1657
  es_dir = utils.FindFile(name, es_base_dir, os.path.isdir)
1658

    
1659
  if es_dir is None:
1660
    return False, ("Directory for External Storage Provider %s not"
1661
                   " found in search path" % name)
1662

    
1663
  # ES Files dictionary, we will populate it with the absolute path
1664
  # names; if the value is True, then it is a required file, otherwise
1665
  # an optional one
1666
  es_files = dict.fromkeys(constants.ES_SCRIPTS, True)
1667

    
1668
  es_files[constants.ES_PARAMETERS_FILE] = True
1669

    
1670
  for (filename, _) in es_files.items():
1671
    es_files[filename] = utils.PathJoin(es_dir, filename)
1672

    
1673
    try:
1674
      st = os.stat(es_files[filename])
1675
    except EnvironmentError, err:
1676
      return False, ("File '%s' under path '%s' is missing (%s)" %
1677
                     (filename, es_dir, utils.ErrnoOrStr(err)))
1678

    
1679
    if not stat.S_ISREG(stat.S_IFMT(st.st_mode)):
1680
      return False, ("File '%s' under path '%s' is not a regular file" %
1681
                     (filename, es_dir))
1682

    
1683
    if filename in constants.ES_SCRIPTS:
1684
      if stat.S_IMODE(st.st_mode) & stat.S_IXUSR != stat.S_IXUSR:
1685
        return False, ("File '%s' under path '%s' is not executable" %
1686
                       (filename, es_dir))
1687

    
1688
  parameters = []
1689
  if constants.ES_PARAMETERS_FILE in es_files:
1690
    parameters_file = es_files[constants.ES_PARAMETERS_FILE]
1691
    try:
1692
      parameters = utils.ReadFile(parameters_file).splitlines()
1693
    except EnvironmentError, err:
1694
      return False, ("Error while reading the EXT parameters file at %s: %s" %
1695
                     (parameters_file, utils.ErrnoOrStr(err)))
1696
    parameters = [v.split(None, 1) for v in parameters]
1697

    
1698
  es_obj = \
1699
    objects.ExtStorage(name=name, path=es_dir,
1700
                       create_script=es_files[constants.ES_SCRIPT_CREATE],
1701
                       remove_script=es_files[constants.ES_SCRIPT_REMOVE],
1702
                       grow_script=es_files[constants.ES_SCRIPT_GROW],
1703
                       attach_script=es_files[constants.ES_SCRIPT_ATTACH],
1704
                       detach_script=es_files[constants.ES_SCRIPT_DETACH],
1705
                       setinfo_script=es_files[constants.ES_SCRIPT_SETINFO],
1706
                       verify_script=es_files[constants.ES_SCRIPT_VERIFY],
1707
                       supported_parameters=parameters)
1708
  return True, es_obj
1709

    
1710

    
1711
def _ExtStorageEnvironment(unique_id, ext_params,
1712
                           size=None, grow=None, metadata=None):
1713
  """Calculate the environment for an External Storage script.
1714

1715
  @type unique_id: tuple (driver, vol_name)
1716
  @param unique_id: ExtStorage pool and name of the Volume
1717
  @type ext_params: dict
1718
  @param ext_params: the EXT parameters
1719
  @type size: string
1720
  @param size: size of the Volume (in mebibytes)
1721
  @type grow: string
1722
  @param grow: new size of Volume after grow (in mebibytes)
1723
  @type metadata: string
1724
  @param metadata: metadata info of the Volume
1725
  @rtype: dict
1726
  @return: dict of environment variables
1727

1728
  """
1729
  vol_name = unique_id[1]
1730

    
1731
  result = {}
1732
  result["VOL_NAME"] = vol_name
1733

    
1734
  # EXT params
1735
  for pname, pvalue in ext_params.items():
1736
    result["EXTP_%s" % pname.upper()] = str(pvalue)
1737

    
1738
  if size is not None:
1739
    result["VOL_SIZE"] = size
1740

    
1741
  if grow is not None:
1742
    result["VOL_NEW_SIZE"] = grow
1743

    
1744
  if metadata is not None:
1745
    result["VOL_METADATA"] = metadata
1746

    
1747
  return result
1748

    
1749

    
1750
def _VolumeLogName(kind, es_name, volume):
1751
  """Compute the ExtStorage log filename for a given Volume and operation.
1752

1753
  @type kind: string
1754
  @param kind: the operation type (e.g. create, remove etc.)
1755
  @type es_name: string
1756
  @param es_name: the ExtStorage name
1757
  @type volume: string
1758
  @param volume: the name of the Volume inside the External Storage
1759

1760
  """
1761
  # Check if the extstorage log dir is a valid dir
1762
  if not os.path.isdir(pathutils.LOG_ES_DIR):
1763
    base.ThrowError("Cannot find log directory: %s", pathutils.LOG_ES_DIR)
1764

    
1765
  # TODO: Use tempfile.mkstemp to create unique filename
1766
  basename = ("%s-%s-%s-%s.log" %
1767
              (kind, es_name, volume, utils.TimestampForFilename()))
1768
  return utils.PathJoin(pathutils.LOG_ES_DIR, basename)
1769

    
1770

    
1771
DEV_MAP = {
1772
  constants.LD_LV: LogicalVolume,
1773
  constants.LD_DRBD8: drbd.DRBD8Dev,
1774
  constants.LD_BLOCKDEV: PersistentBlockDevice,
1775
  constants.LD_RBD: RADOSBlockDevice,
1776
  constants.LD_EXT: ExtStorageDevice,
1777
  }
1778

    
1779
if constants.ENABLE_FILE_STORAGE or constants.ENABLE_SHARED_FILE_STORAGE:
1780
  DEV_MAP[constants.LD_FILE] = FileStorage
1781

    
1782

    
1783
def _VerifyDiskType(dev_type):
1784
  if dev_type not in DEV_MAP:
1785
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1786

    
1787

    
1788
def _VerifyDiskParams(disk):
1789
  """Verifies if all disk parameters are set.
1790

1791
  """
1792
  missing = set(constants.DISK_LD_DEFAULTS[disk.dev_type]) - set(disk.params)
1793
  if missing:
1794
    raise errors.ProgrammerError("Block device is missing disk parameters: %s" %
1795
                                 missing)
1796

    
1797

    
1798
def FindDevice(disk, children):
1799
  """Search for an existing, assembled device.
1800

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

1804
  @type disk: L{objects.Disk}
1805
  @param disk: the disk object to find
1806
  @type children: list of L{bdev.BlockDev}
1807
  @param children: the list of block devices that are children of the device
1808
                  represented by the disk parameter
1809

1810
  """
1811
  _VerifyDiskType(disk.dev_type)
1812
  device = DEV_MAP[disk.dev_type](disk.physical_id, children, disk.size,
1813
                                  disk.params)
1814
  if not device.attached:
1815
    return None
1816
  return device
1817

    
1818

    
1819
def Assemble(disk, children):
1820
  """Try to attach or assemble an existing device.
1821

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

1825
  @type disk: L{objects.Disk}
1826
  @param disk: the disk object to assemble
1827
  @type children: list of L{bdev.BlockDev}
1828
  @param children: the list of block devices that are children of the device
1829
                  represented by the disk parameter
1830

1831
  """
1832
  _VerifyDiskType(disk.dev_type)
1833
  _VerifyDiskParams(disk)
1834
  device = DEV_MAP[disk.dev_type](disk.physical_id, children, disk.size,
1835
                                  disk.params)
1836
  device.Assemble()
1837
  return device
1838

    
1839

    
1840
def Create(disk, children, excl_stor):
1841
  """Create a device.
1842

1843
  @type disk: L{objects.Disk}
1844
  @param disk: the disk object to create
1845
  @type children: list of L{bdev.BlockDev}
1846
  @param children: the list of block devices that are children of the device
1847
                  represented by the disk parameter
1848
  @type excl_stor: boolean
1849
  @param excl_stor: Whether exclusive_storage is active
1850
  @rtype: L{bdev.BlockDev}
1851
  @return: the created device, or C{None} in case of an error
1852

1853
  """
1854
  _VerifyDiskType(disk.dev_type)
1855
  _VerifyDiskParams(disk)
1856
  device = DEV_MAP[disk.dev_type].Create(disk.physical_id, children, disk.size,
1857
                                         disk.spindles, disk.params, excl_stor)
1858
  return device