Statistics
| Branch: | Tag: | Revision:

root / lib / storage / bdev.py @ 688b5752

History | View | Annotate | Download (56.3 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
  _INVALID_NAMES = compat.UniqueFrozenset([".", "..", "snapshot", "pvmove"])
176
  _INVALID_SUBSTRINGS = compat.UniqueFrozenset(["_mlog", "_mimage"])
177

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

319
    """
320
    if not fields:
321
      raise errors.ProgrammerError("No fields specified")
322

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

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

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

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

    
340
      data.append(splitted_fields)
341

    
342
    return data
343

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

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

352
    @rtype: list
353
    @return: list of objects.LvmPvInfo objects
354

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

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

    
399
    return data
400

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

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

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

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

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

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

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

    
452
    return data
453

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

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

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

    
468
  def Remove(self):
469
    """Remove this logical volume.
470

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

    
481
  def Rename(self, new_id):
482
    """Rename this logical volume.
483

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

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

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

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

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

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

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

    
527
    return (status, major, minor, pe_size, stripes)
528

    
529
  @classmethod
530
  def _GetLvInfo(cls, dev_path, _run_cmd=utils.RunCmd):
531
    """Get info about the given existing LV to be used.
532

533
    """
534
    result = _run_cmd(["lvs", "--noheadings", "--separator=,",
535
                       "--units=k", "--nosuffix",
536
                       "-olv_attr,lv_kernel_major,lv_kernel_minor,"
537
                       "vg_extent_size,stripes", dev_path])
538
    if result.failed:
539
      base.ThrowError("Can't find LV %s: %s, %s",
540
                      dev_path, result.fail_reason, result.output)
541
    # the output can (and will) have multiple lines for multi-segment
542
    # LVs, as the 'stripes' parameter is a segment one, so we take
543
    # only the last entry, which is the one we're interested in; note
544
    # that with LVM2 anyway the 'stripes' value must be constant
545
    # across segments, so this is a no-op actually
546
    out = result.stdout.splitlines()
547
    if not out: # totally empty result? splitlines() returns at least
548
                # one line for any non-empty string
549
      base.ThrowError("Can't parse LVS output, no lines? Got '%s'", str(out))
550
    return cls._ParseLvInfoLine(out[-1], ",")
551

    
552
  def Attach(self):
553
    """Attach to an existing LV.
554

555
    This method will try to see if an existing and active LV exists
556
    which matches our name. If so, its major/minor will be
557
    recorded.
558

559
    """
560
    self.attached = False
561
    try:
562
      (status, major, minor, pe_size, stripes) = \
563
        self._GetLvInfo(self.dev_path)
564
    except errors.BlockDeviceError:
565
      return False
566

    
567
    self.major = major
568
    self.minor = minor
569
    self.pe_size = pe_size
570
    self.stripe_count = stripes
571
    self._degraded = status[0] == "v" # virtual volume, i.e. doesn't backing
572
                                      # storage
573
    self.attached = True
574
    return True
575

    
576
  def Assemble(self):
577
    """Assemble the device.
578

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

583
    """
584
    result = utils.RunCmd(["lvchange", "-ay", self.dev_path])
585
    if result.failed:
586
      base.ThrowError("Can't activate lv %s: %s", self.dev_path, result.output)
587

    
588
  def Shutdown(self):
589
    """Shutdown the device.
590

591
    This is a no-op for the LV device type, as we don't deactivate the
592
    volumes on shutdown.
593

594
    """
595
    pass
596

    
597
  def GetSyncStatus(self):
598
    """Returns the sync status of the device.
599

600
    If this device is a mirroring device, this function returns the
601
    status of the mirror.
602

603
    For logical volumes, sync_percent and estimated_time are always
604
    None (no recovery in progress, as we don't handle the mirrored LV
605
    case). The is_degraded parameter is the inverse of the ldisk
606
    parameter.
607

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

614
    The status was already read in Attach, so we just return it.
615

616
    @rtype: objects.BlockDevStatus
617

618
    """
619
    if self._degraded:
620
      ldisk_status = constants.LDS_FAULTY
621
    else:
622
      ldisk_status = constants.LDS_OKAY
623

    
624
    return objects.BlockDevStatus(dev_path=self.dev_path,
625
                                  major=self.major,
626
                                  minor=self.minor,
627
                                  sync_percent=None,
628
                                  estimated_time=None,
629
                                  is_degraded=self._degraded,
630
                                  ldisk_status=ldisk_status)
631

    
632
  def Open(self, force=False):
633
    """Make the device ready for I/O.
634

635
    This is a no-op for the LV device type.
636

637
    """
638
    pass
639

    
640
  def Close(self):
641
    """Notifies that the device will no longer be used for I/O.
642

643
    This is a no-op for the LV device type.
644

645
    """
646
    pass
647

    
648
  def Snapshot(self, size):
649
    """Create a snapshot copy of an lvm block device.
650

651
    @returns: tuple (vg, lv)
652

653
    """
654
    snap_name = self._lv_name + ".snap"
655

    
656
    # remove existing snapshot if found
657
    snap = LogicalVolume((self._vg_name, snap_name), None, size, self.params)
658
    base.IgnoreError(snap.Remove)
659

    
660
    vg_info = self.GetVGInfo([self._vg_name], False)
661
    if not vg_info:
662
      base.ThrowError("Can't compute VG info for vg %s", self._vg_name)
663
    free_size, _, _ = vg_info[0]
664
    if free_size < size:
665
      base.ThrowError("Not enough free space: required %s,"
666
                      " available %s", size, free_size)
667

    
668
    _CheckResult(utils.RunCmd(["lvcreate", "-L%dm" % size, "-s",
669
                               "-n%s" % snap_name, self.dev_path]))
670

    
671
    return (self._vg_name, snap_name)
672

    
673
  def _RemoveOldInfo(self):
674
    """Try to remove old tags from the lv.
675

676
    """
677
    result = utils.RunCmd(["lvs", "-o", "tags", "--noheadings", "--nosuffix",
678
                           self.dev_path])
679
    _CheckResult(result)
680

    
681
    raw_tags = result.stdout.strip()
682
    if raw_tags:
683
      for tag in raw_tags.split(","):
684
        _CheckResult(utils.RunCmd(["lvchange", "--deltag",
685
                                   tag.strip(), self.dev_path]))
686

    
687
  def SetInfo(self, text):
688
    """Update metadata with info text.
689

690
    """
691
    base.BlockDev.SetInfo(self, text)
692

    
693
    self._RemoveOldInfo()
694

    
695
    # Replace invalid characters
696
    text = re.sub("^[^A-Za-z0-9_+.]", "_", text)
697
    text = re.sub("[^-A-Za-z0-9_+.]", "_", text)
698

    
699
    # Only up to 128 characters are allowed
700
    text = text[:128]
701

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

    
704
  def Grow(self, amount, dryrun, backingstore):
705
    """Grow the logical volume.
706

707
    """
708
    if not backingstore:
709
      return
710
    if self.pe_size is None or self.stripe_count is None:
711
      if not self.Attach():
712
        base.ThrowError("Can't attach to LV during Grow()")
713
    full_stripe_size = self.pe_size * self.stripe_count
714
    # pe_size is in KB
715
    amount *= 1024
716
    rest = amount % full_stripe_size
717
    if rest != 0:
718
      amount += full_stripe_size - rest
719
    cmd = ["lvextend", "-L", "+%dk" % amount]
720
    if dryrun:
721
      cmd.append("--test")
722
    # we try multiple algorithms since the 'best' ones might not have
723
    # space available in the right place, but later ones might (since
724
    # they have less constraints); also note that only recent LVM
725
    # supports 'cling'
726
    for alloc_policy in "contiguous", "cling", "normal":
727
      result = utils.RunCmd(cmd + ["--alloc", alloc_policy, self.dev_path])
728
      if not result.failed:
729
        return
730
    base.ThrowError("Can't grow LV %s: %s", self.dev_path, result.output)
731

    
732

    
733
class FileStorage(base.BlockDev):
734
  """File device.
735

736
  This class represents the a file storage backend device.
737

738
  The unique_id for the file device is a (file_driver, file_path) tuple.
739

740
  """
741
  def __init__(self, unique_id, children, size, params):
742
    """Initalizes a file device backend.
743

744
    """
745
    if children:
746
      raise errors.BlockDeviceError("Invalid setup for file device")
747
    super(FileStorage, self).__init__(unique_id, children, size, params)
748
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
749
      raise ValueError("Invalid configuration data %s" % str(unique_id))
750
    self.driver = unique_id[0]
751
    self.dev_path = unique_id[1]
752

    
753
    CheckFileStoragePath(self.dev_path)
754

    
755
    self.Attach()
756

    
757
  def Assemble(self):
758
    """Assemble the device.
759

760
    Checks whether the file device exists, raises BlockDeviceError otherwise.
761

762
    """
763
    if not os.path.exists(self.dev_path):
764
      base.ThrowError("File device '%s' does not exist" % self.dev_path)
765

    
766
  def Shutdown(self):
767
    """Shutdown the device.
768

769
    This is a no-op for the file type, as we don't deactivate
770
    the file on shutdown.
771

772
    """
773
    pass
774

    
775
  def Open(self, force=False):
776
    """Make the device ready for I/O.
777

778
    This is a no-op for the file type.
779

780
    """
781
    pass
782

    
783
  def Close(self):
784
    """Notifies that the device will no longer be used for I/O.
785

786
    This is a no-op for the file type.
787

788
    """
789
    pass
790

    
791
  def Remove(self):
792
    """Remove the file backing the block device.
793

794
    @rtype: boolean
795
    @return: True if the removal was successful
796

797
    """
798
    try:
799
      os.remove(self.dev_path)
800
    except OSError, err:
801
      if err.errno != errno.ENOENT:
802
        base.ThrowError("Can't remove file '%s': %s", self.dev_path, err)
803

    
804
  def Rename(self, new_id):
805
    """Renames the file.
806

807
    """
808
    # TODO: implement rename for file-based storage
809
    base.ThrowError("Rename is not supported for file-based storage")
810

    
811
  def Grow(self, amount, dryrun, backingstore):
812
    """Grow the file
813

814
    @param amount: the amount (in mebibytes) to grow with
815

816
    """
817
    if not backingstore:
818
      return
819
    # Check that the file exists
820
    self.Assemble()
821
    current_size = self.GetActualSize()
822
    new_size = current_size + amount * 1024 * 1024
823
    assert new_size > current_size, "Cannot Grow with a negative amount"
824
    # We can't really simulate the growth
825
    if dryrun:
826
      return
827
    try:
828
      f = open(self.dev_path, "a+")
829
      f.truncate(new_size)
830
      f.close()
831
    except EnvironmentError, err:
832
      base.ThrowError("Error in file growth: %", str(err))
833

    
834
  def Attach(self):
835
    """Attach to an existing file.
836

837
    Check if this file already exists.
838

839
    @rtype: boolean
840
    @return: True if file exists
841

842
    """
843
    self.attached = os.path.exists(self.dev_path)
844
    return self.attached
845

    
846
  def GetActualSize(self):
847
    """Return the actual disk size.
848

849
    @note: the device needs to be active when this is called
850

851
    """
852
    assert self.attached, "BlockDevice not attached in GetActualSize()"
853
    try:
854
      st = os.stat(self.dev_path)
855
      return st.st_size
856
    except OSError, err:
857
      base.ThrowError("Can't stat %s: %s", self.dev_path, err)
858

    
859
  @classmethod
860
  def Create(cls, unique_id, children, size, spindles, params, excl_stor):
861
    """Create a new file.
862

863
    @param size: the size of file in MiB
864

865
    @rtype: L{bdev.FileStorage}
866
    @return: an instance of FileStorage
867

868
    """
869
    if excl_stor:
870
      raise errors.ProgrammerError("FileStorage device requested with"
871
                                   " exclusive_storage")
872
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
873
      raise ValueError("Invalid configuration data %s" % str(unique_id))
874

    
875
    dev_path = unique_id[1]
876

    
877
    CheckFileStoragePath(dev_path)
878

    
879
    try:
880
      fd = os.open(dev_path, os.O_RDWR | os.O_CREAT | os.O_EXCL)
881
      f = os.fdopen(fd, "w")
882
      f.truncate(size * 1024 * 1024)
883
      f.close()
884
    except EnvironmentError, err:
885
      if err.errno == errno.EEXIST:
886
        base.ThrowError("File already existing: %s", dev_path)
887
      base.ThrowError("Error in file creation: %", str(err))
888

    
889
    return FileStorage(unique_id, children, size, params)
890

    
891

    
892
class PersistentBlockDevice(base.BlockDev):
893
  """A block device with persistent node
894

895
  May be either directly attached, or exposed through DM (e.g. dm-multipath).
896
  udev helpers are probably required to give persistent, human-friendly
897
  names.
898

899
  For the time being, pathnames are required to lie under /dev.
900

901
  """
902
  def __init__(self, unique_id, children, size, params):
903
    """Attaches to a static block device.
904

905
    The unique_id is a path under /dev.
906

907
    """
908
    super(PersistentBlockDevice, self).__init__(unique_id, children, size,
909
                                                params)
910
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
911
      raise ValueError("Invalid configuration data %s" % str(unique_id))
912
    self.dev_path = unique_id[1]
913
    if not os.path.realpath(self.dev_path).startswith("/dev/"):
914
      raise ValueError("Full path '%s' lies outside /dev" %
915
                              os.path.realpath(self.dev_path))
916
    # TODO: this is just a safety guard checking that we only deal with devices
917
    # we know how to handle. In the future this will be integrated with
918
    # external storage backends and possible values will probably be collected
919
    # from the cluster configuration.
920
    if unique_id[0] != constants.BLOCKDEV_DRIVER_MANUAL:
921
      raise ValueError("Got persistent block device of invalid type: %s" %
922
                       unique_id[0])
923

    
924
    self.major = self.minor = None
925
    self.Attach()
926

    
927
  @classmethod
928
  def Create(cls, unique_id, children, size, spindles, params, excl_stor):
929
    """Create a new device
930

931
    This is a noop, we only return a PersistentBlockDevice instance
932

933
    """
934
    if excl_stor:
935
      raise errors.ProgrammerError("Persistent block device requested with"
936
                                   " exclusive_storage")
937
    return PersistentBlockDevice(unique_id, children, 0, params)
938

    
939
  def Remove(self):
940
    """Remove a device
941

942
    This is a noop
943

944
    """
945
    pass
946

    
947
  def Rename(self, new_id):
948
    """Rename this device.
949

950
    """
951
    base.ThrowError("Rename is not supported for PersistentBlockDev storage")
952

    
953
  def Attach(self):
954
    """Attach to an existing block device.
955

956

957
    """
958
    self.attached = False
959
    try:
960
      st = os.stat(self.dev_path)
961
    except OSError, err:
962
      logging.error("Error stat()'ing %s: %s", self.dev_path, str(err))
963
      return False
964

    
965
    if not stat.S_ISBLK(st.st_mode):
966
      logging.error("%s is not a block device", self.dev_path)
967
      return False
968

    
969
    self.major = os.major(st.st_rdev)
970
    self.minor = os.minor(st.st_rdev)
971
    self.attached = True
972

    
973
    return True
974

    
975
  def Assemble(self):
976
    """Assemble the device.
977

978
    """
979
    pass
980

    
981
  def Shutdown(self):
982
    """Shutdown the device.
983

984
    """
985
    pass
986

    
987
  def Open(self, force=False):
988
    """Make the device ready for I/O.
989

990
    """
991
    pass
992

    
993
  def Close(self):
994
    """Notifies that the device will no longer be used for I/O.
995

996
    """
997
    pass
998

    
999
  def Grow(self, amount, dryrun, backingstore):
1000
    """Grow the logical volume.
1001

1002
    """
1003
    base.ThrowError("Grow is not supported for PersistentBlockDev storage")
1004

    
1005

    
1006
class RADOSBlockDevice(base.BlockDev):
1007
  """A RADOS Block Device (rbd).
1008

1009
  This class implements the RADOS Block Device for the backend. You need
1010
  the rbd kernel driver, the RADOS Tools and a working RADOS cluster for
1011
  this to be functional.
1012

1013
  """
1014
  def __init__(self, unique_id, children, size, params):
1015
    """Attaches to an rbd device.
1016

1017
    """
1018
    super(RADOSBlockDevice, self).__init__(unique_id, children, size, params)
1019
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1020
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1021

    
1022
    self.driver, self.rbd_name = unique_id
1023

    
1024
    self.major = self.minor = None
1025
    self.Attach()
1026

    
1027
  @classmethod
1028
  def Create(cls, unique_id, children, size, spindles, params, excl_stor):
1029
    """Create a new rbd device.
1030

1031
    Provision a new rbd volume inside a RADOS pool.
1032

1033
    """
1034
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1035
      raise errors.ProgrammerError("Invalid configuration data %s" %
1036
                                   str(unique_id))
1037
    if excl_stor:
1038
      raise errors.ProgrammerError("RBD device requested with"
1039
                                   " exclusive_storage")
1040
    rbd_pool = params[constants.LDP_POOL]
1041
    rbd_name = unique_id[1]
1042

    
1043
    # Provision a new rbd volume (Image) inside the RADOS cluster.
1044
    cmd = [constants.RBD_CMD, "create", "-p", rbd_pool,
1045
           rbd_name, "--size", "%s" % size]
1046
    result = utils.RunCmd(cmd)
1047
    if result.failed:
1048
      base.ThrowError("rbd creation failed (%s): %s",
1049
                      result.fail_reason, result.output)
1050

    
1051
    return RADOSBlockDevice(unique_id, children, size, params)
1052

    
1053
  def Remove(self):
1054
    """Remove the rbd device.
1055

1056
    """
1057
    rbd_pool = self.params[constants.LDP_POOL]
1058
    rbd_name = self.unique_id[1]
1059

    
1060
    if not self.minor and not self.Attach():
1061
      # The rbd device doesn't exist.
1062
      return
1063

    
1064
    # First shutdown the device (remove mappings).
1065
    self.Shutdown()
1066

    
1067
    # Remove the actual Volume (Image) from the RADOS cluster.
1068
    cmd = [constants.RBD_CMD, "rm", "-p", rbd_pool, rbd_name]
1069
    result = utils.RunCmd(cmd)
1070
    if result.failed:
1071
      base.ThrowError("Can't remove Volume from cluster with rbd rm: %s - %s",
1072
                      result.fail_reason, result.output)
1073

    
1074
  def Rename(self, new_id):
1075
    """Rename this device.
1076

1077
    """
1078
    pass
1079

    
1080
  def Attach(self):
1081
    """Attach to an existing rbd device.
1082

1083
    This method maps the rbd volume that matches our name with
1084
    an rbd device and then attaches to this device.
1085

1086
    """
1087
    self.attached = False
1088

    
1089
    # Map the rbd volume to a block device under /dev
1090
    self.dev_path = self._MapVolumeToBlockdev(self.unique_id)
1091

    
1092
    try:
1093
      st = os.stat(self.dev_path)
1094
    except OSError, err:
1095
      logging.error("Error stat()'ing %s: %s", self.dev_path, str(err))
1096
      return False
1097

    
1098
    if not stat.S_ISBLK(st.st_mode):
1099
      logging.error("%s is not a block device", self.dev_path)
1100
      return False
1101

    
1102
    self.major = os.major(st.st_rdev)
1103
    self.minor = os.minor(st.st_rdev)
1104
    self.attached = True
1105

    
1106
    return True
1107

    
1108
  def _MapVolumeToBlockdev(self, unique_id):
1109
    """Maps existing rbd volumes to block devices.
1110

1111
    This method should be idempotent if the mapping already exists.
1112

1113
    @rtype: string
1114
    @return: the block device path that corresponds to the volume
1115

1116
    """
1117
    pool = self.params[constants.LDP_POOL]
1118
    name = unique_id[1]
1119

    
1120
    # Check if the mapping already exists.
1121
    rbd_dev = self._VolumeToBlockdev(pool, name)
1122
    if rbd_dev:
1123
      # The mapping exists. Return it.
1124
      return rbd_dev
1125

    
1126
    # The mapping doesn't exist. Create it.
1127
    map_cmd = [constants.RBD_CMD, "map", "-p", pool, name]
1128
    result = utils.RunCmd(map_cmd)
1129
    if result.failed:
1130
      base.ThrowError("rbd map failed (%s): %s",
1131
                      result.fail_reason, result.output)
1132

    
1133
    # Find the corresponding rbd device.
1134
    rbd_dev = self._VolumeToBlockdev(pool, name)
1135
    if not rbd_dev:
1136
      base.ThrowError("rbd map succeeded, but could not find the rbd block"
1137
                      " device in output of showmapped, for volume: %s", name)
1138

    
1139
    # The device was successfully mapped. Return it.
1140
    return rbd_dev
1141

    
1142
  @classmethod
1143
  def _VolumeToBlockdev(cls, pool, volume_name):
1144
    """Do the 'volume name'-to-'rbd block device' resolving.
1145

1146
    @type pool: string
1147
    @param pool: RADOS pool to use
1148
    @type volume_name: string
1149
    @param volume_name: the name of the volume whose device we search for
1150
    @rtype: string or None
1151
    @return: block device path if the volume is mapped, else None
1152

1153
    """
1154
    try:
1155
      # Newer versions of the rbd tool support json output formatting. Use it
1156
      # if available.
1157
      showmap_cmd = [
1158
        constants.RBD_CMD,
1159
        "showmapped",
1160
        "-p",
1161
        pool,
1162
        "--format",
1163
        "json"
1164
        ]
1165
      result = utils.RunCmd(showmap_cmd)
1166
      if result.failed:
1167
        logging.error("rbd JSON output formatting returned error (%s): %s,"
1168
                      "falling back to plain output parsing",
1169
                      result.fail_reason, result.output)
1170
        raise RbdShowmappedJsonError
1171

    
1172
      return cls._ParseRbdShowmappedJson(result.output, volume_name)
1173
    except RbdShowmappedJsonError:
1174
      # For older versions of rbd, we have to parse the plain / text output
1175
      # manually.
1176
      showmap_cmd = [constants.RBD_CMD, "showmapped", "-p", pool]
1177
      result = utils.RunCmd(showmap_cmd)
1178
      if result.failed:
1179
        base.ThrowError("rbd showmapped failed (%s): %s",
1180
                        result.fail_reason, result.output)
1181

    
1182
      return cls._ParseRbdShowmappedPlain(result.output, volume_name)
1183

    
1184
  @staticmethod
1185
  def _ParseRbdShowmappedJson(output, volume_name):
1186
    """Parse the json output of `rbd showmapped'.
1187

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

1191
    @type output: string
1192
    @param output: the json output of `rbd showmapped'
1193
    @type volume_name: string
1194
    @param volume_name: the name of the volume whose device we search for
1195
    @rtype: string or None
1196
    @return: block device path if the volume is mapped, else None
1197

1198
    """
1199
    try:
1200
      devices = serializer.LoadJson(output)
1201
    except ValueError, err:
1202
      base.ThrowError("Unable to parse JSON data: %s" % err)
1203

    
1204
    rbd_dev = None
1205
    for d in devices.values(): # pylint: disable=E1103
1206
      try:
1207
        name = d["name"]
1208
      except KeyError:
1209
        base.ThrowError("'name' key missing from json object %s", devices)
1210

    
1211
      if name == volume_name:
1212
        if rbd_dev is not None:
1213
          base.ThrowError("rbd volume %s is mapped more than once", volume_name)
1214

    
1215
        rbd_dev = d["device"]
1216

    
1217
    return rbd_dev
1218

    
1219
  @staticmethod
1220
  def _ParseRbdShowmappedPlain(output, volume_name):
1221
    """Parse the (plain / text) output of `rbd showmapped'.
1222

1223
    This method parses the output of `rbd showmapped' and returns
1224
    the rbd block device path (e.g. /dev/rbd0) that matches the
1225
    given rbd volume.
1226

1227
    @type output: string
1228
    @param output: the plain text output of `rbd showmapped'
1229
    @type volume_name: string
1230
    @param volume_name: the name of the volume whose device we search for
1231
    @rtype: string or None
1232
    @return: block device path if the volume is mapped, else None
1233

1234
    """
1235
    allfields = 5
1236
    volumefield = 2
1237
    devicefield = 4
1238

    
1239
    lines = output.splitlines()
1240

    
1241
    # Try parsing the new output format (ceph >= 0.55).
1242
    splitted_lines = map(lambda l: l.split(), lines)
1243

    
1244
    # Check for empty output.
1245
    if not splitted_lines:
1246
      return None
1247

    
1248
    # Check showmapped output, to determine number of fields.
1249
    field_cnt = len(splitted_lines[0])
1250
    if field_cnt != allfields:
1251
      # Parsing the new format failed. Fallback to parsing the old output
1252
      # format (< 0.55).
1253
      splitted_lines = map(lambda l: l.split("\t"), lines)
1254
      if field_cnt != allfields:
1255
        base.ThrowError("Cannot parse rbd showmapped output expected %s fields,"
1256
                        " found %s", allfields, field_cnt)
1257

    
1258
    matched_lines = \
1259
      filter(lambda l: len(l) == allfields and l[volumefield] == volume_name,
1260
             splitted_lines)
1261

    
1262
    if len(matched_lines) > 1:
1263
      base.ThrowError("rbd volume %s mapped more than once", volume_name)
1264

    
1265
    if matched_lines:
1266
      # rbd block device found. Return it.
1267
      rbd_dev = matched_lines[0][devicefield]
1268
      return rbd_dev
1269

    
1270
    # The given volume is not mapped.
1271
    return None
1272

    
1273
  def Assemble(self):
1274
    """Assemble the device.
1275

1276
    """
1277
    pass
1278

    
1279
  def Shutdown(self):
1280
    """Shutdown the device.
1281

1282
    """
1283
    if not self.minor and not self.Attach():
1284
      # The rbd device doesn't exist.
1285
      return
1286

    
1287
    # Unmap the block device from the Volume.
1288
    self._UnmapVolumeFromBlockdev(self.unique_id)
1289

    
1290
    self.minor = None
1291
    self.dev_path = None
1292

    
1293
  def _UnmapVolumeFromBlockdev(self, unique_id):
1294
    """Unmaps the rbd device from the Volume it is mapped.
1295

1296
    Unmaps the rbd device from the Volume it was previously mapped to.
1297
    This method should be idempotent if the Volume isn't mapped.
1298

1299
    """
1300
    pool = self.params[constants.LDP_POOL]
1301
    name = unique_id[1]
1302

    
1303
    # Check if the mapping already exists.
1304
    rbd_dev = self._VolumeToBlockdev(pool, name)
1305

    
1306
    if rbd_dev:
1307
      # The mapping exists. Unmap the rbd device.
1308
      unmap_cmd = [constants.RBD_CMD, "unmap", "%s" % rbd_dev]
1309
      result = utils.RunCmd(unmap_cmd)
1310
      if result.failed:
1311
        base.ThrowError("rbd unmap failed (%s): %s",
1312
                        result.fail_reason, result.output)
1313

    
1314
  def Open(self, force=False):
1315
    """Make the device ready for I/O.
1316

1317
    """
1318
    pass
1319

    
1320
  def Close(self):
1321
    """Notifies that the device will no longer be used for I/O.
1322

1323
    """
1324
    pass
1325

    
1326
  def Grow(self, amount, dryrun, backingstore):
1327
    """Grow the Volume.
1328

1329
    @type amount: integer
1330
    @param amount: the amount (in mebibytes) to grow with
1331
    @type dryrun: boolean
1332
    @param dryrun: whether to execute the operation in simulation mode
1333
        only, without actually increasing the size
1334

1335
    """
1336
    if not backingstore:
1337
      return
1338
    if not self.Attach():
1339
      base.ThrowError("Can't attach to rbd device during Grow()")
1340

    
1341
    if dryrun:
1342
      # the rbd tool does not support dry runs of resize operations.
1343
      # Since rbd volumes are thinly provisioned, we assume
1344
      # there is always enough free space for the operation.
1345
      return
1346

    
1347
    rbd_pool = self.params[constants.LDP_POOL]
1348
    rbd_name = self.unique_id[1]
1349
    new_size = self.size + amount
1350

    
1351
    # Resize the rbd volume (Image) inside the RADOS cluster.
1352
    cmd = [constants.RBD_CMD, "resize", "-p", rbd_pool,
1353
           rbd_name, "--size", "%s" % new_size]
1354
    result = utils.RunCmd(cmd)
1355
    if result.failed:
1356
      base.ThrowError("rbd resize failed (%s): %s",
1357
                      result.fail_reason, result.output)
1358

    
1359

    
1360
class ExtStorageDevice(base.BlockDev):
1361
  """A block device provided by an ExtStorage Provider.
1362

1363
  This class implements the External Storage Interface, which means
1364
  handling of the externally provided block devices.
1365

1366
  """
1367
  def __init__(self, unique_id, children, size, params):
1368
    """Attaches to an extstorage block device.
1369

1370
    """
1371
    super(ExtStorageDevice, self).__init__(unique_id, children, size, params)
1372
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1373
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1374

    
1375
    self.driver, self.vol_name = unique_id
1376
    self.ext_params = params
1377

    
1378
    self.major = self.minor = None
1379
    self.Attach()
1380

    
1381
  @classmethod
1382
  def Create(cls, unique_id, children, size, spindles, params, excl_stor):
1383
    """Create a new extstorage device.
1384

1385
    Provision a new volume using an extstorage provider, which will
1386
    then be mapped to a block device.
1387

1388
    """
1389
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1390
      raise errors.ProgrammerError("Invalid configuration data %s" %
1391
                                   str(unique_id))
1392
    if excl_stor:
1393
      raise errors.ProgrammerError("extstorage device requested with"
1394
                                   " exclusive_storage")
1395

    
1396
    # Call the External Storage's create script,
1397
    # to provision a new Volume inside the External Storage
1398
    _ExtStorageAction(constants.ES_ACTION_CREATE, unique_id,
1399
                      params, str(size))
1400

    
1401
    return ExtStorageDevice(unique_id, children, size, params)
1402

    
1403
  def Remove(self):
1404
    """Remove the extstorage device.
1405

1406
    """
1407
    if not self.minor and not self.Attach():
1408
      # The extstorage device doesn't exist.
1409
      return
1410

    
1411
    # First shutdown the device (remove mappings).
1412
    self.Shutdown()
1413

    
1414
    # Call the External Storage's remove script,
1415
    # to remove the Volume from the External Storage
1416
    _ExtStorageAction(constants.ES_ACTION_REMOVE, self.unique_id,
1417
                      self.ext_params)
1418

    
1419
  def Rename(self, new_id):
1420
    """Rename this device.
1421

1422
    """
1423
    pass
1424

    
1425
  def Attach(self):
1426
    """Attach to an existing extstorage device.
1427

1428
    This method maps the extstorage volume that matches our name with
1429
    a corresponding block device and then attaches to this device.
1430

1431
    """
1432
    self.attached = False
1433

    
1434
    # Call the External Storage's attach script,
1435
    # to attach an existing Volume to a block device under /dev
1436
    self.dev_path = _ExtStorageAction(constants.ES_ACTION_ATTACH,
1437
                                      self.unique_id, self.ext_params)
1438

    
1439
    try:
1440
      st = os.stat(self.dev_path)
1441
    except OSError, err:
1442
      logging.error("Error stat()'ing %s: %s", self.dev_path, str(err))
1443
      return False
1444

    
1445
    if not stat.S_ISBLK(st.st_mode):
1446
      logging.error("%s is not a block device", self.dev_path)
1447
      return False
1448

    
1449
    self.major = os.major(st.st_rdev)
1450
    self.minor = os.minor(st.st_rdev)
1451
    self.attached = True
1452

    
1453
    return True
1454

    
1455
  def Assemble(self):
1456
    """Assemble the device.
1457

1458
    """
1459
    pass
1460

    
1461
  def Shutdown(self):
1462
    """Shutdown the device.
1463

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

    
1469
    # Call the External Storage's detach script,
1470
    # to detach an existing Volume from it's block device under /dev
1471
    _ExtStorageAction(constants.ES_ACTION_DETACH, self.unique_id,
1472
                      self.ext_params)
1473

    
1474
    self.minor = None
1475
    self.dev_path = None
1476

    
1477
  def Open(self, force=False):
1478
    """Make the device ready for I/O.
1479

1480
    """
1481
    pass
1482

    
1483
  def Close(self):
1484
    """Notifies that the device will no longer be used for I/O.
1485

1486
    """
1487
    pass
1488

    
1489
  def Grow(self, amount, dryrun, backingstore):
1490
    """Grow the Volume.
1491

1492
    @type amount: integer
1493
    @param amount: the amount (in mebibytes) to grow with
1494
    @type dryrun: boolean
1495
    @param dryrun: whether to execute the operation in simulation mode
1496
        only, without actually increasing the size
1497

1498
    """
1499
    if not backingstore:
1500
      return
1501
    if not self.Attach():
1502
      base.ThrowError("Can't attach to extstorage device during Grow()")
1503

    
1504
    if dryrun:
1505
      # we do not support dry runs of resize operations for now.
1506
      return
1507

    
1508
    new_size = self.size + amount
1509

    
1510
    # Call the External Storage's grow script,
1511
    # to grow an existing Volume inside the External Storage
1512
    _ExtStorageAction(constants.ES_ACTION_GROW, self.unique_id,
1513
                      self.ext_params, str(self.size), grow=str(new_size))
1514

    
1515
  def SetInfo(self, text):
1516
    """Update metadata with info text.
1517

1518
    """
1519
    # Replace invalid characters
1520
    text = re.sub("^[^A-Za-z0-9_+.]", "_", text)
1521
    text = re.sub("[^-A-Za-z0-9_+.]", "_", text)
1522

    
1523
    # Only up to 128 characters are allowed
1524
    text = text[:128]
1525

    
1526
    # Call the External Storage's setinfo script,
1527
    # to set metadata for an existing Volume inside the External Storage
1528
    _ExtStorageAction(constants.ES_ACTION_SETINFO, self.unique_id,
1529
                      self.ext_params, metadata=text)
1530

    
1531

    
1532
def _ExtStorageAction(action, unique_id, ext_params,
1533
                      size=None, grow=None, metadata=None):
1534
  """Take an External Storage action.
1535

1536
  Take an External Storage action concerning or affecting
1537
  a specific Volume inside the External Storage.
1538

1539
  @type action: string
1540
  @param action: which action to perform. One of:
1541
                 create / remove / grow / attach / detach
1542
  @type unique_id: tuple (driver, vol_name)
1543
  @param unique_id: a tuple containing the type of ExtStorage (driver)
1544
                    and the Volume name
1545
  @type ext_params: dict
1546
  @param ext_params: ExtStorage parameters
1547
  @type size: integer
1548
  @param size: the size of the Volume in mebibytes
1549
  @type grow: integer
1550
  @param grow: the new size in mebibytes (after grow)
1551
  @type metadata: string
1552
  @param metadata: metadata info of the Volume, for use by the provider
1553
  @rtype: None or a block device path (during attach)
1554

1555
  """
1556
  driver, vol_name = unique_id
1557

    
1558
  # Create an External Storage instance of type `driver'
1559
  status, inst_es = ExtStorageFromDisk(driver)
1560
  if not status:
1561
    base.ThrowError("%s" % inst_es)
1562

    
1563
  # Create the basic environment for the driver's scripts
1564
  create_env = _ExtStorageEnvironment(unique_id, ext_params, size,
1565
                                      grow, metadata)
1566

    
1567
  # Do not use log file for action `attach' as we need
1568
  # to get the output from RunResult
1569
  # TODO: find a way to have a log file for attach too
1570
  logfile = None
1571
  if action is not constants.ES_ACTION_ATTACH:
1572
    logfile = _VolumeLogName(action, driver, vol_name)
1573

    
1574
  # Make sure the given action results in a valid script
1575
  if action not in constants.ES_SCRIPTS:
1576
    base.ThrowError("Action '%s' doesn't result in a valid ExtStorage script" %
1577
                    action)
1578

    
1579
  # Find out which external script to run according the given action
1580
  script_name = action + "_script"
1581
  script = getattr(inst_es, script_name)
1582

    
1583
  # Run the external script
1584
  result = utils.RunCmd([script], env=create_env,
1585
                        cwd=inst_es.path, output=logfile,)
1586
  if result.failed:
1587
    logging.error("External storage's %s command '%s' returned"
1588
                  " error: %s, logfile: %s, output: %s",
1589
                  action, result.cmd, result.fail_reason,
1590
                  logfile, result.output)
1591

    
1592
    # If logfile is 'None' (during attach), it breaks TailFile
1593
    # TODO: have a log file for attach too
1594
    if action is not constants.ES_ACTION_ATTACH:
1595
      lines = [utils.SafeEncode(val)
1596
               for val in utils.TailFile(logfile, lines=20)]
1597
    else:
1598
      lines = result.output[-20:]
1599

    
1600
    base.ThrowError("External storage's %s script failed (%s), last"
1601
                    " lines of output:\n%s",
1602
                    action, result.fail_reason, "\n".join(lines))
1603

    
1604
  if action == constants.ES_ACTION_ATTACH:
1605
    return result.stdout
1606

    
1607

    
1608
def ExtStorageFromDisk(name, base_dir=None):
1609
  """Create an ExtStorage instance from disk.
1610

1611
  This function will return an ExtStorage instance
1612
  if the given name is a valid ExtStorage name.
1613

1614
  @type base_dir: string
1615
  @keyword base_dir: Base directory containing ExtStorage installations.
1616
                     Defaults to a search in all the ES_SEARCH_PATH dirs.
1617
  @rtype: tuple
1618
  @return: True and the ExtStorage instance if we find a valid one, or
1619
      False and the diagnose message on error
1620

1621
  """
1622
  if base_dir is None:
1623
    es_base_dir = pathutils.ES_SEARCH_PATH
1624
  else:
1625
    es_base_dir = [base_dir]
1626

    
1627
  es_dir = utils.FindFile(name, es_base_dir, os.path.isdir)
1628

    
1629
  if es_dir is None:
1630
    return False, ("Directory for External Storage Provider %s not"
1631
                   " found in search path" % name)
1632

    
1633
  # ES Files dictionary, we will populate it with the absolute path
1634
  # names; if the value is True, then it is a required file, otherwise
1635
  # an optional one
1636
  es_files = dict.fromkeys(constants.ES_SCRIPTS, True)
1637

    
1638
  es_files[constants.ES_PARAMETERS_FILE] = True
1639

    
1640
  for (filename, _) in es_files.items():
1641
    es_files[filename] = utils.PathJoin(es_dir, filename)
1642

    
1643
    try:
1644
      st = os.stat(es_files[filename])
1645
    except EnvironmentError, err:
1646
      return False, ("File '%s' under path '%s' is missing (%s)" %
1647
                     (filename, es_dir, utils.ErrnoOrStr(err)))
1648

    
1649
    if not stat.S_ISREG(stat.S_IFMT(st.st_mode)):
1650
      return False, ("File '%s' under path '%s' is not a regular file" %
1651
                     (filename, es_dir))
1652

    
1653
    if filename in constants.ES_SCRIPTS:
1654
      if stat.S_IMODE(st.st_mode) & stat.S_IXUSR != stat.S_IXUSR:
1655
        return False, ("File '%s' under path '%s' is not executable" %
1656
                       (filename, es_dir))
1657

    
1658
  parameters = []
1659
  if constants.ES_PARAMETERS_FILE in es_files:
1660
    parameters_file = es_files[constants.ES_PARAMETERS_FILE]
1661
    try:
1662
      parameters = utils.ReadFile(parameters_file).splitlines()
1663
    except EnvironmentError, err:
1664
      return False, ("Error while reading the EXT parameters file at %s: %s" %
1665
                     (parameters_file, utils.ErrnoOrStr(err)))
1666
    parameters = [v.split(None, 1) for v in parameters]
1667

    
1668
  es_obj = \
1669
    objects.ExtStorage(name=name, path=es_dir,
1670
                       create_script=es_files[constants.ES_SCRIPT_CREATE],
1671
                       remove_script=es_files[constants.ES_SCRIPT_REMOVE],
1672
                       grow_script=es_files[constants.ES_SCRIPT_GROW],
1673
                       attach_script=es_files[constants.ES_SCRIPT_ATTACH],
1674
                       detach_script=es_files[constants.ES_SCRIPT_DETACH],
1675
                       setinfo_script=es_files[constants.ES_SCRIPT_SETINFO],
1676
                       verify_script=es_files[constants.ES_SCRIPT_VERIFY],
1677
                       supported_parameters=parameters)
1678
  return True, es_obj
1679

    
1680

    
1681
def _ExtStorageEnvironment(unique_id, ext_params,
1682
                           size=None, grow=None, metadata=None):
1683
  """Calculate the environment for an External Storage script.
1684

1685
  @type unique_id: tuple (driver, vol_name)
1686
  @param unique_id: ExtStorage pool and name of the Volume
1687
  @type ext_params: dict
1688
  @param ext_params: the EXT parameters
1689
  @type size: string
1690
  @param size: size of the Volume (in mebibytes)
1691
  @type grow: string
1692
  @param grow: new size of Volume after grow (in mebibytes)
1693
  @type metadata: string
1694
  @param metadata: metadata info of the Volume
1695
  @rtype: dict
1696
  @return: dict of environment variables
1697

1698
  """
1699
  vol_name = unique_id[1]
1700

    
1701
  result = {}
1702
  result["VOL_NAME"] = vol_name
1703

    
1704
  # EXT params
1705
  for pname, pvalue in ext_params.items():
1706
    result["EXTP_%s" % pname.upper()] = str(pvalue)
1707

    
1708
  if size is not None:
1709
    result["VOL_SIZE"] = size
1710

    
1711
  if grow is not None:
1712
    result["VOL_NEW_SIZE"] = grow
1713

    
1714
  if metadata is not None:
1715
    result["VOL_METADATA"] = metadata
1716

    
1717
  return result
1718

    
1719

    
1720
def _VolumeLogName(kind, es_name, volume):
1721
  """Compute the ExtStorage log filename for a given Volume and operation.
1722

1723
  @type kind: string
1724
  @param kind: the operation type (e.g. create, remove etc.)
1725
  @type es_name: string
1726
  @param es_name: the ExtStorage name
1727
  @type volume: string
1728
  @param volume: the name of the Volume inside the External Storage
1729

1730
  """
1731
  # Check if the extstorage log dir is a valid dir
1732
  if not os.path.isdir(pathutils.LOG_ES_DIR):
1733
    base.ThrowError("Cannot find log directory: %s", pathutils.LOG_ES_DIR)
1734

    
1735
  # TODO: Use tempfile.mkstemp to create unique filename
1736
  basename = ("%s-%s-%s-%s.log" %
1737
              (kind, es_name, volume, utils.TimestampForFilename()))
1738
  return utils.PathJoin(pathutils.LOG_ES_DIR, basename)
1739

    
1740

    
1741
DEV_MAP = {
1742
  constants.LD_LV: LogicalVolume,
1743
  constants.LD_DRBD8: drbd.DRBD8Dev,
1744
  constants.LD_BLOCKDEV: PersistentBlockDevice,
1745
  constants.LD_RBD: RADOSBlockDevice,
1746
  constants.LD_EXT: ExtStorageDevice,
1747
  }
1748

    
1749
if constants.ENABLE_FILE_STORAGE or constants.ENABLE_SHARED_FILE_STORAGE:
1750
  DEV_MAP[constants.LD_FILE] = FileStorage
1751

    
1752

    
1753
def _VerifyDiskType(dev_type):
1754
  if dev_type not in DEV_MAP:
1755
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1756

    
1757

    
1758
def _VerifyDiskParams(disk):
1759
  """Verifies if all disk parameters are set.
1760

1761
  """
1762
  missing = set(constants.DISK_LD_DEFAULTS[disk.dev_type]) - set(disk.params)
1763
  if missing:
1764
    raise errors.ProgrammerError("Block device is missing disk parameters: %s" %
1765
                                 missing)
1766

    
1767

    
1768
def FindDevice(disk, children):
1769
  """Search for an existing, assembled device.
1770

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

1774
  @type disk: L{objects.Disk}
1775
  @param disk: the disk object to find
1776
  @type children: list of L{bdev.BlockDev}
1777
  @param children: the list of block devices that are children of the device
1778
                  represented by the disk parameter
1779

1780
  """
1781
  _VerifyDiskType(disk.dev_type)
1782
  device = DEV_MAP[disk.dev_type](disk.physical_id, children, disk.size,
1783
                                  disk.params)
1784
  if not device.attached:
1785
    return None
1786
  return device
1787

    
1788

    
1789
def Assemble(disk, children):
1790
  """Try to attach or assemble an existing device.
1791

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

1795
  @type disk: L{objects.Disk}
1796
  @param disk: the disk object to assemble
1797
  @type children: list of L{bdev.BlockDev}
1798
  @param children: the list of block devices that are children of the device
1799
                  represented by the disk parameter
1800

1801
  """
1802
  _VerifyDiskType(disk.dev_type)
1803
  _VerifyDiskParams(disk)
1804
  device = DEV_MAP[disk.dev_type](disk.physical_id, children, disk.size,
1805
                                  disk.params)
1806
  device.Assemble()
1807
  return device
1808

    
1809

    
1810
def Create(disk, children, excl_stor):
1811
  """Create a device.
1812

1813
  @type disk: L{objects.Disk}
1814
  @param disk: the disk object to create
1815
  @type children: list of L{bdev.BlockDev}
1816
  @param children: the list of block devices that are children of the device
1817
                  represented by the disk parameter
1818
  @type excl_stor: boolean
1819
  @param excl_stor: Whether exclusive_storage is active
1820
  @rtype: L{bdev.BlockDev}
1821
  @return: the created device, or C{None} in case of an error
1822

1823
  """
1824
  _VerifyDiskType(disk.dev_type)
1825
  _VerifyDiskParams(disk)
1826
  device = DEV_MAP[disk.dev_type].Create(disk.physical_id, children, disk.size,
1827
                                         disk.spindles, disk.params, excl_stor)
1828
  return device