Statistics
| Branch: | Tag: | Revision:

root / lib / block / bdev.py @ d01e51a5

History | View | Annotate | Download (55.4 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.block import drbd
39
from ganeti.block 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, 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
      pvlist = cls._GetEmptyPvNames(pvs_info, req_pvs)
269
      current_pvs = len(pvlist)
270
      if current_pvs < req_pvs:
271
        base.ThrowError("Not enough empty PVs to create a disk of %d MB:"
272
                        " %d available, %d needed", size, current_pvs, req_pvs)
273
      assert current_pvs == len(pvlist)
274
      if stripes > current_pvs:
275
        # No warning issued for this, as it's no surprise
276
        stripes = current_pvs
277

    
278
    else:
279
      if stripes < desired_stripes:
280
        logging.warning("Could not use %d stripes for VG %s, as only %d PVs are"
281
                        " available.", desired_stripes, vg_name, current_pvs)
282
      free_size = sum([pv.free for pv in pvs_info])
283
      # The size constraint should have been checked from the master before
284
      # calling the create function.
285
      if free_size < size:
286
        base.ThrowError("Not enough free space: required %s,"
287
                        " available %s", size, free_size)
288

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

    
303
  @staticmethod
304
  def _GetVolumeInfo(lvm_cmd, fields):
305
    """Returns LVM Volume infos using lvm_cmd
306

307
    @param lvm_cmd: Should be one of "pvs", "vgs" or "lvs"
308
    @param fields: Fields to return
309
    @return: A list of dicts each with the parsed fields
310

311
    """
312
    if not fields:
313
      raise errors.ProgrammerError("No fields specified")
314

    
315
    sep = "|"
316
    cmd = [lvm_cmd, "--noheadings", "--nosuffix", "--units=m", "--unbuffered",
317
           "--separator=%s" % sep, "-o%s" % ",".join(fields)]
318

    
319
    result = utils.RunCmd(cmd)
320
    if result.failed:
321
      raise errors.CommandError("Can't get the volume information: %s - %s" %
322
                                (result.fail_reason, result.output))
323

    
324
    data = []
325
    for line in result.stdout.splitlines():
326
      splitted_fields = line.strip().split(sep)
327

    
328
      if len(fields) != len(splitted_fields):
329
        raise errors.CommandError("Can't parse %s output: line '%s'" %
330
                                  (lvm_cmd, line))
331

    
332
      data.append(splitted_fields)
333

    
334
    return data
335

    
336
  @classmethod
337
  def GetPVInfo(cls, vg_names, filter_allocatable=True, include_lvs=False):
338
    """Get the free space info for PVs in a volume group.
339

340
    @param vg_names: list of volume group names, if empty all will be returned
341
    @param filter_allocatable: whether to skip over unallocatable PVs
342
    @param include_lvs: whether to include a list of LVs hosted on each PV
343

344
    @rtype: list
345
    @return: list of objects.LvmPvInfo objects
346

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

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

    
391
    return data
392

    
393
  @classmethod
394
  def _GetExclusiveStorageVgFree(cls, vg_name):
395
    """Return the free disk space in the given VG, in exclusive storage mode.
396

397
    @type vg_name: string
398
    @param vg_name: VG name
399
    @rtype: float
400
    @return: free space in MiB
401
    """
402
    pvs_info = cls.GetPVInfo([vg_name])
403
    if not pvs_info:
404
      return 0.0
405
    pv_size = cls._GetStdPvSize(pvs_info)
406
    num_pvs = len(cls._GetEmptyPvNames(pvs_info))
407
    return pv_size * num_pvs
408

    
409
  @classmethod
410
  def GetVGInfo(cls, vg_names, excl_stor, filter_readonly=True):
411
    """Get the free space info for specific VGs.
412

413
    @param vg_names: list of volume group names, if empty all will be returned
414
    @param excl_stor: whether exclusive_storage is enabled
415
    @param filter_readonly: whether to skip over readonly VGs
416

417
    @rtype: list
418
    @return: list of tuples (free_space, total_size, name) with free_space in
419
             MiB
420

421
    """
422
    try:
423
      info = cls._GetVolumeInfo("vgs", ["vg_name", "vg_free", "vg_attr",
424
                                        "vg_size"])
425
    except errors.GenericError, err:
426
      logging.error("Can't get VG information: %s", err)
427
      return None
428

    
429
    data = []
430
    for vg_name, vg_free, vg_attr, vg_size in info:
431
      # (possibly) skip over vgs which are not writable
432
      if filter_readonly and vg_attr[0] == "r":
433
        continue
434
      # (possibly) skip over vgs which are not in the right volume group(s)
435
      if vg_names and vg_name not in vg_names:
436
        continue
437
      # Exclusive storage needs a different concept of free space
438
      if excl_stor:
439
        es_free = cls._GetExclusiveStorageVgFree(vg_name)
440
        assert es_free <= vg_free
441
        vg_free = es_free
442
      data.append((float(vg_free), float(vg_size), vg_name))
443

    
444
    return data
445

    
446
  @classmethod
447
  def _ValidateName(cls, name):
448
    """Validates that a given name is valid as VG or LV name.
449

450
    The list of valid characters and restricted names is taken out of
451
    the lvm(8) manpage, with the simplification that we enforce both
452
    VG and LV restrictions on the names.
453

454
    """
455
    if (not cls._VALID_NAME_RE.match(name) or
456
        name in cls._INVALID_NAMES or
457
        compat.any(substring in name for substring in cls._INVALID_SUBSTRINGS)):
458
      base.ThrowError("Invalid LVM name '%s'", name)
459

    
460
  def Remove(self):
461
    """Remove this logical volume.
462

463
    """
464
    if not self.minor and not self.Attach():
465
      # the LV does not exist
466
      return
467
    result = utils.RunCmd(["lvremove", "-f", "%s/%s" %
468
                           (self._vg_name, self._lv_name)])
469
    if result.failed:
470
      base.ThrowError("Can't lvremove: %s - %s",
471
                      result.fail_reason, result.output)
472

    
473
  def Rename(self, new_id):
474
    """Rename this logical volume.
475

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

    
490
  def Attach(self):
491
    """Attach to an existing LV.
492

493
    This method will try to see if an existing and active LV exists
494
    which matches our name. If so, its major/minor will be
495
    recorded.
496

497
    """
498
    self.attached = False
499
    result = utils.RunCmd(["lvs", "--noheadings", "--separator=,",
500
                           "--units=m", "--nosuffix",
501
                           "-olv_attr,lv_kernel_major,lv_kernel_minor,"
502
                           "vg_extent_size,stripes", self.dev_path])
503
    if result.failed:
504
      logging.error("Can't find LV %s: %s, %s",
505
                    self.dev_path, result.fail_reason, result.output)
506
      return False
507
    # the output can (and will) have multiple lines for multi-segment
508
    # LVs, as the 'stripes' parameter is a segment one, so we take
509
    # only the last entry, which is the one we're interested in; note
510
    # that with LVM2 anyway the 'stripes' value must be constant
511
    # across segments, so this is a no-op actually
512
    out = result.stdout.splitlines()
513
    if not out: # totally empty result? splitlines() returns at least
514
                # one line for any non-empty string
515
      logging.error("Can't parse LVS output, no lines? Got '%s'", str(out))
516
      return False
517
    out = out[-1].strip().rstrip(",")
518
    out = out.split(",")
519
    if len(out) != 5:
520
      logging.error("Can't parse LVS output, len(%s) != 5", str(out))
521
      return False
522

    
523
    status, major, minor, pe_size, stripes = out
524
    if len(status) < 6:
525
      logging.error("lvs lv_attr is not at least 6 characters (%s)", status)
526
      return False
527

    
528
    try:
529
      major = int(major)
530
      minor = int(minor)
531
    except (TypeError, ValueError), err:
532
      logging.error("lvs major/minor cannot be parsed: %s", str(err))
533

    
534
    try:
535
      pe_size = int(float(pe_size))
536
    except (TypeError, ValueError), err:
537
      logging.error("Can't parse vg extent size: %s", err)
538
      return False
539

    
540
    try:
541
      stripes = int(stripes)
542
    except (TypeError, ValueError), err:
543
      logging.error("Can't parse the number of stripes: %s", err)
544
      return False
545

    
546
    self.major = major
547
    self.minor = minor
548
    self.pe_size = pe_size
549
    self.stripe_count = stripes
550
    self._degraded = status[0] == "v" # virtual volume, i.e. doesn't backing
551
                                      # storage
552
    self.attached = True
553
    return True
554

    
555
  def Assemble(self):
556
    """Assemble the device.
557

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

562
    """
563
    result = utils.RunCmd(["lvchange", "-ay", self.dev_path])
564
    if result.failed:
565
      base.ThrowError("Can't activate lv %s: %s", self.dev_path, result.output)
566

    
567
  def Shutdown(self):
568
    """Shutdown the device.
569

570
    This is a no-op for the LV device type, as we don't deactivate the
571
    volumes on shutdown.
572

573
    """
574
    pass
575

    
576
  def GetSyncStatus(self):
577
    """Returns the sync status of the device.
578

579
    If this device is a mirroring device, this function returns the
580
    status of the mirror.
581

582
    For logical volumes, sync_percent and estimated_time are always
583
    None (no recovery in progress, as we don't handle the mirrored LV
584
    case). The is_degraded parameter is the inverse of the ldisk
585
    parameter.
586

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

593
    The status was already read in Attach, so we just return it.
594

595
    @rtype: objects.BlockDevStatus
596

597
    """
598
    if self._degraded:
599
      ldisk_status = constants.LDS_FAULTY
600
    else:
601
      ldisk_status = constants.LDS_OKAY
602

    
603
    return objects.BlockDevStatus(dev_path=self.dev_path,
604
                                  major=self.major,
605
                                  minor=self.minor,
606
                                  sync_percent=None,
607
                                  estimated_time=None,
608
                                  is_degraded=self._degraded,
609
                                  ldisk_status=ldisk_status)
610

    
611
  def Open(self, force=False):
612
    """Make the device ready for I/O.
613

614
    This is a no-op for the LV device type.
615

616
    """
617
    pass
618

    
619
  def Close(self):
620
    """Notifies that the device will no longer be used for I/O.
621

622
    This is a no-op for the LV device type.
623

624
    """
625
    pass
626

    
627
  def Snapshot(self, size):
628
    """Create a snapshot copy of an lvm block device.
629

630
    @returns: tuple (vg, lv)
631

632
    """
633
    snap_name = self._lv_name + ".snap"
634

    
635
    # remove existing snapshot if found
636
    snap = LogicalVolume((self._vg_name, snap_name), None, size, self.params)
637
    base.IgnoreError(snap.Remove)
638

    
639
    vg_info = self.GetVGInfo([self._vg_name], False)
640
    if not vg_info:
641
      base.ThrowError("Can't compute VG info for vg %s", self._vg_name)
642
    free_size, _, _ = vg_info[0]
643
    if free_size < size:
644
      base.ThrowError("Not enough free space: required %s,"
645
                      " available %s", size, free_size)
646

    
647
    _CheckResult(utils.RunCmd(["lvcreate", "-L%dm" % size, "-s",
648
                               "-n%s" % snap_name, self.dev_path]))
649

    
650
    return (self._vg_name, snap_name)
651

    
652
  def _RemoveOldInfo(self):
653
    """Try to remove old tags from the lv.
654

655
    """
656
    result = utils.RunCmd(["lvs", "-o", "tags", "--noheadings", "--nosuffix",
657
                           self.dev_path])
658
    _CheckResult(result)
659

    
660
    raw_tags = result.stdout.strip()
661
    if raw_tags:
662
      for tag in raw_tags.split(","):
663
        _CheckResult(utils.RunCmd(["lvchange", "--deltag",
664
                                   tag.strip(), self.dev_path]))
665

    
666
  def SetInfo(self, text):
667
    """Update metadata with info text.
668

669
    """
670
    base.BlockDev.SetInfo(self, text)
671

    
672
    self._RemoveOldInfo()
673

    
674
    # Replace invalid characters
675
    text = re.sub("^[^A-Za-z0-9_+.]", "_", text)
676
    text = re.sub("[^-A-Za-z0-9_+.]", "_", text)
677

    
678
    # Only up to 128 characters are allowed
679
    text = text[:128]
680

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

    
683
  def Grow(self, amount, dryrun, backingstore):
684
    """Grow the logical volume.
685

686
    """
687
    if not backingstore:
688
      return
689
    if self.pe_size is None or self.stripe_count is None:
690
      if not self.Attach():
691
        base.ThrowError("Can't attach to LV during Grow()")
692
    full_stripe_size = self.pe_size * self.stripe_count
693
    rest = amount % full_stripe_size
694
    if rest != 0:
695
      amount += full_stripe_size - rest
696
    cmd = ["lvextend", "-L", "+%dm" % amount]
697
    if dryrun:
698
      cmd.append("--test")
699
    # we try multiple algorithms since the 'best' ones might not have
700
    # space available in the right place, but later ones might (since
701
    # they have less constraints); also note that only recent LVM
702
    # supports 'cling'
703
    for alloc_policy in "contiguous", "cling", "normal":
704
      result = utils.RunCmd(cmd + ["--alloc", alloc_policy, self.dev_path])
705
      if not result.failed:
706
        return
707
    base.ThrowError("Can't grow LV %s: %s", self.dev_path, result.output)
708

    
709

    
710
class FileStorage(base.BlockDev):
711
  """File device.
712

713
  This class represents the a file storage backend device.
714

715
  The unique_id for the file device is a (file_driver, file_path) tuple.
716

717
  """
718
  def __init__(self, unique_id, children, size, params):
719
    """Initalizes a file device backend.
720

721
    """
722
    if children:
723
      raise errors.BlockDeviceError("Invalid setup for file device")
724
    super(FileStorage, self).__init__(unique_id, children, size, params)
725
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
726
      raise ValueError("Invalid configuration data %s" % str(unique_id))
727
    self.driver = unique_id[0]
728
    self.dev_path = unique_id[1]
729

    
730
    CheckFileStoragePath(self.dev_path)
731

    
732
    self.Attach()
733

    
734
  def Assemble(self):
735
    """Assemble the device.
736

737
    Checks whether the file device exists, raises BlockDeviceError otherwise.
738

739
    """
740
    if not os.path.exists(self.dev_path):
741
      base.ThrowError("File device '%s' does not exist" % self.dev_path)
742

    
743
  def Shutdown(self):
744
    """Shutdown the device.
745

746
    This is a no-op for the file type, as we don't deactivate
747
    the file on shutdown.
748

749
    """
750
    pass
751

    
752
  def Open(self, force=False):
753
    """Make the device ready for I/O.
754

755
    This is a no-op for the file type.
756

757
    """
758
    pass
759

    
760
  def Close(self):
761
    """Notifies that the device will no longer be used for I/O.
762

763
    This is a no-op for the file type.
764

765
    """
766
    pass
767

    
768
  def Remove(self):
769
    """Remove the file backing the block device.
770

771
    @rtype: boolean
772
    @return: True if the removal was successful
773

774
    """
775
    try:
776
      os.remove(self.dev_path)
777
    except OSError, err:
778
      if err.errno != errno.ENOENT:
779
        base.ThrowError("Can't remove file '%s': %s", self.dev_path, err)
780

    
781
  def Rename(self, new_id):
782
    """Renames the file.
783

784
    """
785
    # TODO: implement rename for file-based storage
786
    base.ThrowError("Rename is not supported for file-based storage")
787

    
788
  def Grow(self, amount, dryrun, backingstore):
789
    """Grow the file
790

791
    @param amount: the amount (in mebibytes) to grow with
792

793
    """
794
    if not backingstore:
795
      return
796
    # Check that the file exists
797
    self.Assemble()
798
    current_size = self.GetActualSize()
799
    new_size = current_size + amount * 1024 * 1024
800
    assert new_size > current_size, "Cannot Grow with a negative amount"
801
    # We can't really simulate the growth
802
    if dryrun:
803
      return
804
    try:
805
      f = open(self.dev_path, "a+")
806
      f.truncate(new_size)
807
      f.close()
808
    except EnvironmentError, err:
809
      base.ThrowError("Error in file growth: %", str(err))
810

    
811
  def Attach(self):
812
    """Attach to an existing file.
813

814
    Check if this file already exists.
815

816
    @rtype: boolean
817
    @return: True if file exists
818

819
    """
820
    self.attached = os.path.exists(self.dev_path)
821
    return self.attached
822

    
823
  def GetActualSize(self):
824
    """Return the actual disk size.
825

826
    @note: the device needs to be active when this is called
827

828
    """
829
    assert self.attached, "BlockDevice not attached in GetActualSize()"
830
    try:
831
      st = os.stat(self.dev_path)
832
      return st.st_size
833
    except OSError, err:
834
      base.ThrowError("Can't stat %s: %s", self.dev_path, err)
835

    
836
  @classmethod
837
  def Create(cls, unique_id, children, size, params, excl_stor):
838
    """Create a new file.
839

840
    @param size: the size of file in MiB
841

842
    @rtype: L{bdev.FileStorage}
843
    @return: an instance of FileStorage
844

845
    """
846
    if excl_stor:
847
      raise errors.ProgrammerError("FileStorage device requested with"
848
                                   " exclusive_storage")
849
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
850
      raise ValueError("Invalid configuration data %s" % str(unique_id))
851

    
852
    dev_path = unique_id[1]
853

    
854
    CheckFileStoragePath(dev_path)
855

    
856
    try:
857
      fd = os.open(dev_path, os.O_RDWR | os.O_CREAT | os.O_EXCL)
858
      f = os.fdopen(fd, "w")
859
      f.truncate(size * 1024 * 1024)
860
      f.close()
861
    except EnvironmentError, err:
862
      if err.errno == errno.EEXIST:
863
        base.ThrowError("File already existing: %s", dev_path)
864
      base.ThrowError("Error in file creation: %", str(err))
865

    
866
    return FileStorage(unique_id, children, size, params)
867

    
868

    
869
class PersistentBlockDevice(base.BlockDev):
870
  """A block device with persistent node
871

872
  May be either directly attached, or exposed through DM (e.g. dm-multipath).
873
  udev helpers are probably required to give persistent, human-friendly
874
  names.
875

876
  For the time being, pathnames are required to lie under /dev.
877

878
  """
879
  def __init__(self, unique_id, children, size, params):
880
    """Attaches to a static block device.
881

882
    The unique_id is a path under /dev.
883

884
    """
885
    super(PersistentBlockDevice, self).__init__(unique_id, children, size,
886
                                                params)
887
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
888
      raise ValueError("Invalid configuration data %s" % str(unique_id))
889
    self.dev_path = unique_id[1]
890
    if not os.path.realpath(self.dev_path).startswith("/dev/"):
891
      raise ValueError("Full path '%s' lies outside /dev" %
892
                              os.path.realpath(self.dev_path))
893
    # TODO: this is just a safety guard checking that we only deal with devices
894
    # we know how to handle. In the future this will be integrated with
895
    # external storage backends and possible values will probably be collected
896
    # from the cluster configuration.
897
    if unique_id[0] != constants.BLOCKDEV_DRIVER_MANUAL:
898
      raise ValueError("Got persistent block device of invalid type: %s" %
899
                       unique_id[0])
900

    
901
    self.major = self.minor = None
902
    self.Attach()
903

    
904
  @classmethod
905
  def Create(cls, unique_id, children, size, params, excl_stor):
906
    """Create a new device
907

908
    This is a noop, we only return a PersistentBlockDevice instance
909

910
    """
911
    if excl_stor:
912
      raise errors.ProgrammerError("Persistent block device requested with"
913
                                   " exclusive_storage")
914
    return PersistentBlockDevice(unique_id, children, 0, params)
915

    
916
  def Remove(self):
917
    """Remove a device
918

919
    This is a noop
920

921
    """
922
    pass
923

    
924
  def Rename(self, new_id):
925
    """Rename this device.
926

927
    """
928
    base.ThrowError("Rename is not supported for PersistentBlockDev storage")
929

    
930
  def Attach(self):
931
    """Attach to an existing block device.
932

933

934
    """
935
    self.attached = False
936
    try:
937
      st = os.stat(self.dev_path)
938
    except OSError, err:
939
      logging.error("Error stat()'ing %s: %s", self.dev_path, str(err))
940
      return False
941

    
942
    if not stat.S_ISBLK(st.st_mode):
943
      logging.error("%s is not a block device", self.dev_path)
944
      return False
945

    
946
    self.major = os.major(st.st_rdev)
947
    self.minor = os.minor(st.st_rdev)
948
    self.attached = True
949

    
950
    return True
951

    
952
  def Assemble(self):
953
    """Assemble the device.
954

955
    """
956
    pass
957

    
958
  def Shutdown(self):
959
    """Shutdown the device.
960

961
    """
962
    pass
963

    
964
  def Open(self, force=False):
965
    """Make the device ready for I/O.
966

967
    """
968
    pass
969

    
970
  def Close(self):
971
    """Notifies that the device will no longer be used for I/O.
972

973
    """
974
    pass
975

    
976
  def Grow(self, amount, dryrun, backingstore):
977
    """Grow the logical volume.
978

979
    """
980
    base.ThrowError("Grow is not supported for PersistentBlockDev storage")
981

    
982

    
983
class RADOSBlockDevice(base.BlockDev):
984
  """A RADOS Block Device (rbd).
985

986
  This class implements the RADOS Block Device for the backend. You need
987
  the rbd kernel driver, the RADOS Tools and a working RADOS cluster for
988
  this to be functional.
989

990
  """
991
  def __init__(self, unique_id, children, size, params):
992
    """Attaches to an rbd device.
993

994
    """
995
    super(RADOSBlockDevice, self).__init__(unique_id, children, size, params)
996
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
997
      raise ValueError("Invalid configuration data %s" % str(unique_id))
998

    
999
    self.driver, self.rbd_name = unique_id
1000

    
1001
    self.major = self.minor = None
1002
    self.Attach()
1003

    
1004
  @classmethod
1005
  def Create(cls, unique_id, children, size, params, excl_stor):
1006
    """Create a new rbd device.
1007

1008
    Provision a new rbd volume inside a RADOS pool.
1009

1010
    """
1011
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1012
      raise errors.ProgrammerError("Invalid configuration data %s" %
1013
                                   str(unique_id))
1014
    if excl_stor:
1015
      raise errors.ProgrammerError("RBD device requested with"
1016
                                   " exclusive_storage")
1017
    rbd_pool = params[constants.LDP_POOL]
1018
    rbd_name = unique_id[1]
1019

    
1020
    # Provision a new rbd volume (Image) inside the RADOS cluster.
1021
    cmd = [constants.RBD_CMD, "create", "-p", rbd_pool,
1022
           rbd_name, "--size", "%s" % size]
1023
    result = utils.RunCmd(cmd)
1024
    if result.failed:
1025
      base.ThrowError("rbd creation failed (%s): %s",
1026
                      result.fail_reason, result.output)
1027

    
1028
    return RADOSBlockDevice(unique_id, children, size, params)
1029

    
1030
  def Remove(self):
1031
    """Remove the rbd device.
1032

1033
    """
1034
    rbd_pool = self.params[constants.LDP_POOL]
1035
    rbd_name = self.unique_id[1]
1036

    
1037
    if not self.minor and not self.Attach():
1038
      # The rbd device doesn't exist.
1039
      return
1040

    
1041
    # First shutdown the device (remove mappings).
1042
    self.Shutdown()
1043

    
1044
    # Remove the actual Volume (Image) from the RADOS cluster.
1045
    cmd = [constants.RBD_CMD, "rm", "-p", rbd_pool, rbd_name]
1046
    result = utils.RunCmd(cmd)
1047
    if result.failed:
1048
      base.ThrowError("Can't remove Volume from cluster with rbd rm: %s - %s",
1049
                      result.fail_reason, result.output)
1050

    
1051
  def Rename(self, new_id):
1052
    """Rename this device.
1053

1054
    """
1055
    pass
1056

    
1057
  def Attach(self):
1058
    """Attach to an existing rbd device.
1059

1060
    This method maps the rbd volume that matches our name with
1061
    an rbd device and then attaches to this device.
1062

1063
    """
1064
    self.attached = False
1065

    
1066
    # Map the rbd volume to a block device under /dev
1067
    self.dev_path = self._MapVolumeToBlockdev(self.unique_id)
1068

    
1069
    try:
1070
      st = os.stat(self.dev_path)
1071
    except OSError, err:
1072
      logging.error("Error stat()'ing %s: %s", self.dev_path, str(err))
1073
      return False
1074

    
1075
    if not stat.S_ISBLK(st.st_mode):
1076
      logging.error("%s is not a block device", self.dev_path)
1077
      return False
1078

    
1079
    self.major = os.major(st.st_rdev)
1080
    self.minor = os.minor(st.st_rdev)
1081
    self.attached = True
1082

    
1083
    return True
1084

    
1085
  def _MapVolumeToBlockdev(self, unique_id):
1086
    """Maps existing rbd volumes to block devices.
1087

1088
    This method should be idempotent if the mapping already exists.
1089

1090
    @rtype: string
1091
    @return: the block device path that corresponds to the volume
1092

1093
    """
1094
    pool = self.params[constants.LDP_POOL]
1095
    name = unique_id[1]
1096

    
1097
    # Check if the mapping already exists.
1098
    rbd_dev = self._VolumeToBlockdev(pool, name)
1099
    if rbd_dev:
1100
      # The mapping exists. Return it.
1101
      return rbd_dev
1102

    
1103
    # The mapping doesn't exist. Create it.
1104
    map_cmd = [constants.RBD_CMD, "map", "-p", pool, name]
1105
    result = utils.RunCmd(map_cmd)
1106
    if result.failed:
1107
      base.ThrowError("rbd map failed (%s): %s",
1108
                      result.fail_reason, result.output)
1109

    
1110
    # Find the corresponding rbd device.
1111
    rbd_dev = self._VolumeToBlockdev(pool, name)
1112
    if not rbd_dev:
1113
      base.ThrowError("rbd map succeeded, but could not find the rbd block"
1114
                      " device in output of showmapped, for volume: %s", name)
1115

    
1116
    # The device was successfully mapped. Return it.
1117
    return rbd_dev
1118

    
1119
  @classmethod
1120
  def _VolumeToBlockdev(cls, pool, volume_name):
1121
    """Do the 'volume name'-to-'rbd block device' resolving.
1122

1123
    @type pool: string
1124
    @param pool: RADOS pool to use
1125
    @type volume_name: string
1126
    @param volume_name: the name of the volume whose device we search for
1127
    @rtype: string or None
1128
    @return: block device path if the volume is mapped, else None
1129

1130
    """
1131
    try:
1132
      # Newer versions of the rbd tool support json output formatting. Use it
1133
      # if available.
1134
      showmap_cmd = [
1135
        constants.RBD_CMD,
1136
        "showmapped",
1137
        "-p",
1138
        pool,
1139
        "--format",
1140
        "json"
1141
        ]
1142
      result = utils.RunCmd(showmap_cmd)
1143
      if result.failed:
1144
        logging.error("rbd JSON output formatting returned error (%s): %s,"
1145
                      "falling back to plain output parsing",
1146
                      result.fail_reason, result.output)
1147
        raise RbdShowmappedJsonError
1148

    
1149
      return cls._ParseRbdShowmappedJson(result.output, volume_name)
1150
    except RbdShowmappedJsonError:
1151
      # For older versions of rbd, we have to parse the plain / text output
1152
      # manually.
1153
      showmap_cmd = [constants.RBD_CMD, "showmapped", "-p", pool]
1154
      result = utils.RunCmd(showmap_cmd)
1155
      if result.failed:
1156
        base.ThrowError("rbd showmapped failed (%s): %s",
1157
                        result.fail_reason, result.output)
1158

    
1159
      return cls._ParseRbdShowmappedPlain(result.output, volume_name)
1160

    
1161
  @staticmethod
1162
  def _ParseRbdShowmappedJson(output, volume_name):
1163
    """Parse the json output of `rbd showmapped'.
1164

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

1168
    @type output: string
1169
    @param output: the json output of `rbd showmapped'
1170
    @type volume_name: string
1171
    @param volume_name: the name of the volume whose device we search for
1172
    @rtype: string or None
1173
    @return: block device path if the volume is mapped, else None
1174

1175
    """
1176
    try:
1177
      devices = serializer.LoadJson(output)
1178
    except ValueError, err:
1179
      base.ThrowError("Unable to parse JSON data: %s" % err)
1180

    
1181
    rbd_dev = None
1182
    for d in devices.values(): # pylint: disable=E1103
1183
      try:
1184
        name = d["name"]
1185
      except KeyError:
1186
        base.ThrowError("'name' key missing from json object %s", devices)
1187

    
1188
      if name == volume_name:
1189
        if rbd_dev is not None:
1190
          base.ThrowError("rbd volume %s is mapped more than once", volume_name)
1191

    
1192
        rbd_dev = d["device"]
1193

    
1194
    return rbd_dev
1195

    
1196
  @staticmethod
1197
  def _ParseRbdShowmappedPlain(output, volume_name):
1198
    """Parse the (plain / text) output of `rbd showmapped'.
1199

1200
    This method parses the output of `rbd showmapped' and returns
1201
    the rbd block device path (e.g. /dev/rbd0) that matches the
1202
    given rbd volume.
1203

1204
    @type output: string
1205
    @param output: the plain text output of `rbd showmapped'
1206
    @type volume_name: string
1207
    @param volume_name: the name of the volume whose device we search for
1208
    @rtype: string or None
1209
    @return: block device path if the volume is mapped, else None
1210

1211
    """
1212
    allfields = 5
1213
    volumefield = 2
1214
    devicefield = 4
1215

    
1216
    lines = output.splitlines()
1217

    
1218
    # Try parsing the new output format (ceph >= 0.55).
1219
    splitted_lines = map(lambda l: l.split(), lines)
1220

    
1221
    # Check for empty output.
1222
    if not splitted_lines:
1223
      return None
1224

    
1225
    # Check showmapped output, to determine number of fields.
1226
    field_cnt = len(splitted_lines[0])
1227
    if field_cnt != allfields:
1228
      # Parsing the new format failed. Fallback to parsing the old output
1229
      # format (< 0.55).
1230
      splitted_lines = map(lambda l: l.split("\t"), lines)
1231
      if field_cnt != allfields:
1232
        base.ThrowError("Cannot parse rbd showmapped output expected %s fields,"
1233
                        " found %s", allfields, field_cnt)
1234

    
1235
    matched_lines = \
1236
      filter(lambda l: len(l) == allfields and l[volumefield] == volume_name,
1237
             splitted_lines)
1238

    
1239
    if len(matched_lines) > 1:
1240
      base.ThrowError("rbd volume %s mapped more than once", volume_name)
1241

    
1242
    if matched_lines:
1243
      # rbd block device found. Return it.
1244
      rbd_dev = matched_lines[0][devicefield]
1245
      return rbd_dev
1246

    
1247
    # The given volume is not mapped.
1248
    return None
1249

    
1250
  def Assemble(self):
1251
    """Assemble the device.
1252

1253
    """
1254
    pass
1255

    
1256
  def Shutdown(self):
1257
    """Shutdown the device.
1258

1259
    """
1260
    if not self.minor and not self.Attach():
1261
      # The rbd device doesn't exist.
1262
      return
1263

    
1264
    # Unmap the block device from the Volume.
1265
    self._UnmapVolumeFromBlockdev(self.unique_id)
1266

    
1267
    self.minor = None
1268
    self.dev_path = None
1269

    
1270
  def _UnmapVolumeFromBlockdev(self, unique_id):
1271
    """Unmaps the rbd device from the Volume it is mapped.
1272

1273
    Unmaps the rbd device from the Volume it was previously mapped to.
1274
    This method should be idempotent if the Volume isn't mapped.
1275

1276
    """
1277
    pool = self.params[constants.LDP_POOL]
1278
    name = unique_id[1]
1279

    
1280
    # Check if the mapping already exists.
1281
    rbd_dev = self._VolumeToBlockdev(pool, name)
1282

    
1283
    if rbd_dev:
1284
      # The mapping exists. Unmap the rbd device.
1285
      unmap_cmd = [constants.RBD_CMD, "unmap", "%s" % rbd_dev]
1286
      result = utils.RunCmd(unmap_cmd)
1287
      if result.failed:
1288
        base.ThrowError("rbd unmap failed (%s): %s",
1289
                        result.fail_reason, result.output)
1290

    
1291
  def Open(self, force=False):
1292
    """Make the device ready for I/O.
1293

1294
    """
1295
    pass
1296

    
1297
  def Close(self):
1298
    """Notifies that the device will no longer be used for I/O.
1299

1300
    """
1301
    pass
1302

    
1303
  def Grow(self, amount, dryrun, backingstore):
1304
    """Grow the Volume.
1305

1306
    @type amount: integer
1307
    @param amount: the amount (in mebibytes) to grow with
1308
    @type dryrun: boolean
1309
    @param dryrun: whether to execute the operation in simulation mode
1310
        only, without actually increasing the size
1311

1312
    """
1313
    if not backingstore:
1314
      return
1315
    if not self.Attach():
1316
      base.ThrowError("Can't attach to rbd device during Grow()")
1317

    
1318
    if dryrun:
1319
      # the rbd tool does not support dry runs of resize operations.
1320
      # Since rbd volumes are thinly provisioned, we assume
1321
      # there is always enough free space for the operation.
1322
      return
1323

    
1324
    rbd_pool = self.params[constants.LDP_POOL]
1325
    rbd_name = self.unique_id[1]
1326
    new_size = self.size + amount
1327

    
1328
    # Resize the rbd volume (Image) inside the RADOS cluster.
1329
    cmd = [constants.RBD_CMD, "resize", "-p", rbd_pool,
1330
           rbd_name, "--size", "%s" % new_size]
1331
    result = utils.RunCmd(cmd)
1332
    if result.failed:
1333
      base.ThrowError("rbd resize failed (%s): %s",
1334
                      result.fail_reason, result.output)
1335

    
1336

    
1337
class ExtStorageDevice(base.BlockDev):
1338
  """A block device provided by an ExtStorage Provider.
1339

1340
  This class implements the External Storage Interface, which means
1341
  handling of the externally provided block devices.
1342

1343
  """
1344
  def __init__(self, unique_id, children, size, params):
1345
    """Attaches to an extstorage block device.
1346

1347
    """
1348
    super(ExtStorageDevice, self).__init__(unique_id, children, size, params)
1349
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1350
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1351

    
1352
    self.driver, self.vol_name = unique_id
1353
    self.ext_params = params
1354

    
1355
    self.major = self.minor = None
1356
    self.Attach()
1357

    
1358
  @classmethod
1359
  def Create(cls, unique_id, children, size, params, excl_stor):
1360
    """Create a new extstorage device.
1361

1362
    Provision a new volume using an extstorage provider, which will
1363
    then be mapped to a block device.
1364

1365
    """
1366
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1367
      raise errors.ProgrammerError("Invalid configuration data %s" %
1368
                                   str(unique_id))
1369
    if excl_stor:
1370
      raise errors.ProgrammerError("extstorage device requested with"
1371
                                   " exclusive_storage")
1372

    
1373
    # Call the External Storage's create script,
1374
    # to provision a new Volume inside the External Storage
1375
    _ExtStorageAction(constants.ES_ACTION_CREATE, unique_id,
1376
                      params, str(size))
1377

    
1378
    return ExtStorageDevice(unique_id, children, size, params)
1379

    
1380
  def Remove(self):
1381
    """Remove the extstorage device.
1382

1383
    """
1384
    if not self.minor and not self.Attach():
1385
      # The extstorage device doesn't exist.
1386
      return
1387

    
1388
    # First shutdown the device (remove mappings).
1389
    self.Shutdown()
1390

    
1391
    # Call the External Storage's remove script,
1392
    # to remove the Volume from the External Storage
1393
    _ExtStorageAction(constants.ES_ACTION_REMOVE, self.unique_id,
1394
                      self.ext_params)
1395

    
1396
  def Rename(self, new_id):
1397
    """Rename this device.
1398

1399
    """
1400
    pass
1401

    
1402
  def Attach(self):
1403
    """Attach to an existing extstorage device.
1404

1405
    This method maps the extstorage volume that matches our name with
1406
    a corresponding block device and then attaches to this device.
1407

1408
    """
1409
    self.attached = False
1410

    
1411
    # Call the External Storage's attach script,
1412
    # to attach an existing Volume to a block device under /dev
1413
    self.dev_path = _ExtStorageAction(constants.ES_ACTION_ATTACH,
1414
                                      self.unique_id, self.ext_params)
1415

    
1416
    try:
1417
      st = os.stat(self.dev_path)
1418
    except OSError, err:
1419
      logging.error("Error stat()'ing %s: %s", self.dev_path, str(err))
1420
      return False
1421

    
1422
    if not stat.S_ISBLK(st.st_mode):
1423
      logging.error("%s is not a block device", self.dev_path)
1424
      return False
1425

    
1426
    self.major = os.major(st.st_rdev)
1427
    self.minor = os.minor(st.st_rdev)
1428
    self.attached = True
1429

    
1430
    return True
1431

    
1432
  def Assemble(self):
1433
    """Assemble the device.
1434

1435
    """
1436
    pass
1437

    
1438
  def Shutdown(self):
1439
    """Shutdown the device.
1440

1441
    """
1442
    if not self.minor and not self.Attach():
1443
      # The extstorage device doesn't exist.
1444
      return
1445

    
1446
    # Call the External Storage's detach script,
1447
    # to detach an existing Volume from it's block device under /dev
1448
    _ExtStorageAction(constants.ES_ACTION_DETACH, self.unique_id,
1449
                      self.ext_params)
1450

    
1451
    self.minor = None
1452
    self.dev_path = None
1453

    
1454
  def Open(self, force=False):
1455
    """Make the device ready for I/O.
1456

1457
    """
1458
    pass
1459

    
1460
  def Close(self):
1461
    """Notifies that the device will no longer be used for I/O.
1462

1463
    """
1464
    pass
1465

    
1466
  def Grow(self, amount, dryrun, backingstore):
1467
    """Grow the Volume.
1468

1469
    @type amount: integer
1470
    @param amount: the amount (in mebibytes) to grow with
1471
    @type dryrun: boolean
1472
    @param dryrun: whether to execute the operation in simulation mode
1473
        only, without actually increasing the size
1474

1475
    """
1476
    if not backingstore:
1477
      return
1478
    if not self.Attach():
1479
      base.ThrowError("Can't attach to extstorage device during Grow()")
1480

    
1481
    if dryrun:
1482
      # we do not support dry runs of resize operations for now.
1483
      return
1484

    
1485
    new_size = self.size + amount
1486

    
1487
    # Call the External Storage's grow script,
1488
    # to grow an existing Volume inside the External Storage
1489
    _ExtStorageAction(constants.ES_ACTION_GROW, self.unique_id,
1490
                      self.ext_params, str(self.size), grow=str(new_size))
1491

    
1492
  def SetInfo(self, text):
1493
    """Update metadata with info text.
1494

1495
    """
1496
    # Replace invalid characters
1497
    text = re.sub("^[^A-Za-z0-9_+.]", "_", text)
1498
    text = re.sub("[^-A-Za-z0-9_+.]", "_", text)
1499

    
1500
    # Only up to 128 characters are allowed
1501
    text = text[:128]
1502

    
1503
    # Call the External Storage's setinfo script,
1504
    # to set metadata for an existing Volume inside the External Storage
1505
    _ExtStorageAction(constants.ES_ACTION_SETINFO, self.unique_id,
1506
                      self.ext_params, metadata=text)
1507

    
1508

    
1509
def _ExtStorageAction(action, unique_id, ext_params,
1510
                      size=None, grow=None, metadata=None):
1511
  """Take an External Storage action.
1512

1513
  Take an External Storage action concerning or affecting
1514
  a specific Volume inside the External Storage.
1515

1516
  @type action: string
1517
  @param action: which action to perform. One of:
1518
                 create / remove / grow / attach / detach
1519
  @type unique_id: tuple (driver, vol_name)
1520
  @param unique_id: a tuple containing the type of ExtStorage (driver)
1521
                    and the Volume name
1522
  @type ext_params: dict
1523
  @param ext_params: ExtStorage parameters
1524
  @type size: integer
1525
  @param size: the size of the Volume in mebibytes
1526
  @type grow: integer
1527
  @param grow: the new size in mebibytes (after grow)
1528
  @type metadata: string
1529
  @param metadata: metadata info of the Volume, for use by the provider
1530
  @rtype: None or a block device path (during attach)
1531

1532
  """
1533
  driver, vol_name = unique_id
1534

    
1535
  # Create an External Storage instance of type `driver'
1536
  status, inst_es = ExtStorageFromDisk(driver)
1537
  if not status:
1538
    base.ThrowError("%s" % inst_es)
1539

    
1540
  # Create the basic environment for the driver's scripts
1541
  create_env = _ExtStorageEnvironment(unique_id, ext_params, size,
1542
                                      grow, metadata)
1543

    
1544
  # Do not use log file for action `attach' as we need
1545
  # to get the output from RunResult
1546
  # TODO: find a way to have a log file for attach too
1547
  logfile = None
1548
  if action is not constants.ES_ACTION_ATTACH:
1549
    logfile = _VolumeLogName(action, driver, vol_name)
1550

    
1551
  # Make sure the given action results in a valid script
1552
  if action not in constants.ES_SCRIPTS:
1553
    base.ThrowError("Action '%s' doesn't result in a valid ExtStorage script" %
1554
                    action)
1555

    
1556
  # Find out which external script to run according the given action
1557
  script_name = action + "_script"
1558
  script = getattr(inst_es, script_name)
1559

    
1560
  # Run the external script
1561
  result = utils.RunCmd([script], env=create_env,
1562
                        cwd=inst_es.path, output=logfile,)
1563
  if result.failed:
1564
    logging.error("External storage's %s command '%s' returned"
1565
                  " error: %s, logfile: %s, output: %s",
1566
                  action, result.cmd, result.fail_reason,
1567
                  logfile, result.output)
1568

    
1569
    # If logfile is 'None' (during attach), it breaks TailFile
1570
    # TODO: have a log file for attach too
1571
    if action is not constants.ES_ACTION_ATTACH:
1572
      lines = [utils.SafeEncode(val)
1573
               for val in utils.TailFile(logfile, lines=20)]
1574
    else:
1575
      lines = result.output[-20:]
1576

    
1577
    base.ThrowError("External storage's %s script failed (%s), last"
1578
                    " lines of output:\n%s",
1579
                    action, result.fail_reason, "\n".join(lines))
1580

    
1581
  if action == constants.ES_ACTION_ATTACH:
1582
    return result.stdout
1583

    
1584

    
1585
def ExtStorageFromDisk(name, base_dir=None):
1586
  """Create an ExtStorage instance from disk.
1587

1588
  This function will return an ExtStorage instance
1589
  if the given name is a valid ExtStorage name.
1590

1591
  @type base_dir: string
1592
  @keyword base_dir: Base directory containing ExtStorage installations.
1593
                     Defaults to a search in all the ES_SEARCH_PATH dirs.
1594
  @rtype: tuple
1595
  @return: True and the ExtStorage instance if we find a valid one, or
1596
      False and the diagnose message on error
1597

1598
  """
1599
  if base_dir is None:
1600
    es_base_dir = pathutils.ES_SEARCH_PATH
1601
  else:
1602
    es_base_dir = [base_dir]
1603

    
1604
  es_dir = utils.FindFile(name, es_base_dir, os.path.isdir)
1605

    
1606
  if es_dir is None:
1607
    return False, ("Directory for External Storage Provider %s not"
1608
                   " found in search path" % name)
1609

    
1610
  # ES Files dictionary, we will populate it with the absolute path
1611
  # names; if the value is True, then it is a required file, otherwise
1612
  # an optional one
1613
  es_files = dict.fromkeys(constants.ES_SCRIPTS, True)
1614

    
1615
  es_files[constants.ES_PARAMETERS_FILE] = True
1616

    
1617
  for (filename, _) in es_files.items():
1618
    es_files[filename] = utils.PathJoin(es_dir, filename)
1619

    
1620
    try:
1621
      st = os.stat(es_files[filename])
1622
    except EnvironmentError, err:
1623
      return False, ("File '%s' under path '%s' is missing (%s)" %
1624
                     (filename, es_dir, utils.ErrnoOrStr(err)))
1625

    
1626
    if not stat.S_ISREG(stat.S_IFMT(st.st_mode)):
1627
      return False, ("File '%s' under path '%s' is not a regular file" %
1628
                     (filename, es_dir))
1629

    
1630
    if filename in constants.ES_SCRIPTS:
1631
      if stat.S_IMODE(st.st_mode) & stat.S_IXUSR != stat.S_IXUSR:
1632
        return False, ("File '%s' under path '%s' is not executable" %
1633
                       (filename, es_dir))
1634

    
1635
  parameters = []
1636
  if constants.ES_PARAMETERS_FILE in es_files:
1637
    parameters_file = es_files[constants.ES_PARAMETERS_FILE]
1638
    try:
1639
      parameters = utils.ReadFile(parameters_file).splitlines()
1640
    except EnvironmentError, err:
1641
      return False, ("Error while reading the EXT parameters file at %s: %s" %
1642
                     (parameters_file, utils.ErrnoOrStr(err)))
1643
    parameters = [v.split(None, 1) for v in parameters]
1644

    
1645
  es_obj = \
1646
    objects.ExtStorage(name=name, path=es_dir,
1647
                       create_script=es_files[constants.ES_SCRIPT_CREATE],
1648
                       remove_script=es_files[constants.ES_SCRIPT_REMOVE],
1649
                       grow_script=es_files[constants.ES_SCRIPT_GROW],
1650
                       attach_script=es_files[constants.ES_SCRIPT_ATTACH],
1651
                       detach_script=es_files[constants.ES_SCRIPT_DETACH],
1652
                       setinfo_script=es_files[constants.ES_SCRIPT_SETINFO],
1653
                       verify_script=es_files[constants.ES_SCRIPT_VERIFY],
1654
                       supported_parameters=parameters)
1655
  return True, es_obj
1656

    
1657

    
1658
def _ExtStorageEnvironment(unique_id, ext_params,
1659
                           size=None, grow=None, metadata=None):
1660
  """Calculate the environment for an External Storage script.
1661

1662
  @type unique_id: tuple (driver, vol_name)
1663
  @param unique_id: ExtStorage pool and name of the Volume
1664
  @type ext_params: dict
1665
  @param ext_params: the EXT parameters
1666
  @type size: string
1667
  @param size: size of the Volume (in mebibytes)
1668
  @type grow: string
1669
  @param grow: new size of Volume after grow (in mebibytes)
1670
  @type metadata: string
1671
  @param metadata: metadata info of the Volume
1672
  @rtype: dict
1673
  @return: dict of environment variables
1674

1675
  """
1676
  vol_name = unique_id[1]
1677

    
1678
  result = {}
1679
  result["VOL_NAME"] = vol_name
1680

    
1681
  # EXT params
1682
  for pname, pvalue in ext_params.items():
1683
    result["EXTP_%s" % pname.upper()] = str(pvalue)
1684

    
1685
  if size is not None:
1686
    result["VOL_SIZE"] = size
1687

    
1688
  if grow is not None:
1689
    result["VOL_NEW_SIZE"] = grow
1690

    
1691
  if metadata is not None:
1692
    result["VOL_METADATA"] = metadata
1693

    
1694
  return result
1695

    
1696

    
1697
def _VolumeLogName(kind, es_name, volume):
1698
  """Compute the ExtStorage log filename for a given Volume and operation.
1699

1700
  @type kind: string
1701
  @param kind: the operation type (e.g. create, remove etc.)
1702
  @type es_name: string
1703
  @param es_name: the ExtStorage name
1704
  @type volume: string
1705
  @param volume: the name of the Volume inside the External Storage
1706

1707
  """
1708
  # Check if the extstorage log dir is a valid dir
1709
  if not os.path.isdir(pathutils.LOG_ES_DIR):
1710
    base.ThrowError("Cannot find log directory: %s", pathutils.LOG_ES_DIR)
1711

    
1712
  # TODO: Use tempfile.mkstemp to create unique filename
1713
  basename = ("%s-%s-%s-%s.log" %
1714
              (kind, es_name, volume, utils.TimestampForFilename()))
1715
  return utils.PathJoin(pathutils.LOG_ES_DIR, basename)
1716

    
1717

    
1718
DEV_MAP = {
1719
  constants.LD_LV: LogicalVolume,
1720
  constants.LD_DRBD8: drbd.DRBD8,
1721
  constants.LD_BLOCKDEV: PersistentBlockDevice,
1722
  constants.LD_RBD: RADOSBlockDevice,
1723
  constants.LD_EXT: ExtStorageDevice,
1724
  }
1725

    
1726
if constants.ENABLE_FILE_STORAGE or constants.ENABLE_SHARED_FILE_STORAGE:
1727
  DEV_MAP[constants.LD_FILE] = FileStorage
1728

    
1729

    
1730
def _VerifyDiskType(dev_type):
1731
  if dev_type not in DEV_MAP:
1732
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1733

    
1734

    
1735
def _VerifyDiskParams(disk):
1736
  """Verifies if all disk parameters are set.
1737

1738
  """
1739
  missing = set(constants.DISK_LD_DEFAULTS[disk.dev_type]) - set(disk.params)
1740
  if missing:
1741
    raise errors.ProgrammerError("Block device is missing disk parameters: %s" %
1742
                                 missing)
1743

    
1744

    
1745
def FindDevice(disk, children):
1746
  """Search for an existing, assembled device.
1747

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

1751
  @type disk: L{objects.Disk}
1752
  @param disk: the disk object to find
1753
  @type children: list of L{bdev.BlockDev}
1754
  @param children: the list of block devices that are children of the device
1755
                  represented by the disk parameter
1756

1757
  """
1758
  _VerifyDiskType(disk.dev_type)
1759
  device = DEV_MAP[disk.dev_type](disk.physical_id, children, disk.size,
1760
                                  disk.params)
1761
  if not device.attached:
1762
    return None
1763
  return device
1764

    
1765

    
1766
def Assemble(disk, children):
1767
  """Try to attach or assemble an existing device.
1768

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

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

1778
  """
1779
  _VerifyDiskType(disk.dev_type)
1780
  _VerifyDiskParams(disk)
1781
  device = DEV_MAP[disk.dev_type](disk.physical_id, children, disk.size,
1782
                                  disk.params)
1783
  device.Assemble()
1784
  return device
1785

    
1786

    
1787
def Create(disk, children, excl_stor):
1788
  """Create a device.
1789

1790
  @type disk: L{objects.Disk}
1791
  @param disk: the disk object to create
1792
  @type children: list of L{bdev.BlockDev}
1793
  @param children: the list of block devices that are children of the device
1794
                  represented by the disk parameter
1795
  @type excl_stor: boolean
1796
  @param excl_stor: Whether exclusive_storage is active
1797

1798
  """
1799
  _VerifyDiskType(disk.dev_type)
1800
  _VerifyDiskParams(disk)
1801
  device = DEV_MAP[disk.dev_type].Create(disk.physical_id, children, disk.size,
1802
                                         disk.params, excl_stor)
1803
  return device