Statistics
| Branch: | Tag: | Revision:

root / lib / storage / bdev.py @ 5073fa0c

History | View | Annotate | Download (55.5 kB)

1
#
2
#
3

    
4
# Copyright (C) 2006, 2007, 2010, 2011, 2012, 2013 Google Inc.
5
#
6
# This program is free software; you can redistribute it and/or modify
7
# it under the terms of the GNU General Public License as published by
8
# the Free Software Foundation; either version 2 of the License, or
9
# (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful, but
12
# WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14
# General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19
# 02110-1301, USA.
20

    
21

    
22
"""Block device abstraction"""
23

    
24
import re
25
import errno
26
import stat
27
import os
28
import logging
29
import math
30

    
31
from ganeti import utils
32
from ganeti import errors
33
from ganeti import constants
34
from ganeti import objects
35
from ganeti import compat
36
from ganeti import pathutils
37
from ganeti import serializer
38
from ganeti.storage import drbd
39
from ganeti.storage import base
40

    
41

    
42
class RbdShowmappedJsonError(Exception):
43
  """`rbd showmmapped' JSON formatting error Exception class.
44

45
  """
46
  pass
47

    
48

    
49
def _CheckResult(result):
50
  """Throws an error if the given result is a failed one.
51

52
  @param result: result from RunCmd
53

54
  """
55
  if result.failed:
56
    base.ThrowError("Command: %s error: %s - %s",
57
                    result.cmd, result.fail_reason, result.output)
58

    
59

    
60
def _GetForbiddenFileStoragePaths():
61
  """Builds a list of path prefixes which shouldn't be used for file storage.
62

63
  @rtype: frozenset
64

65
  """
66
  paths = set([
67
    "/boot",
68
    "/dev",
69
    "/etc",
70
    "/home",
71
    "/proc",
72
    "/root",
73
    "/sys",
74
    ])
75

    
76
  for prefix in ["", "/usr", "/usr/local"]:
77
    paths.update(map(lambda s: "%s/%s" % (prefix, s),
78
                     ["bin", "lib", "lib32", "lib64", "sbin"]))
79

    
80
  return compat.UniqueFrozenset(map(os.path.normpath, paths))
81

    
82

    
83
def _ComputeWrongFileStoragePaths(paths,
84
                                  _forbidden=_GetForbiddenFileStoragePaths()):
85
  """Cross-checks a list of paths for prefixes considered bad.
86

87
  Some paths, e.g. "/bin", should not be used for file storage.
88

89
  @type paths: list
90
  @param paths: List of paths to be checked
91
  @rtype: list
92
  @return: Sorted list of paths for which the user should be warned
93

94
  """
95
  def _Check(path):
96
    return (not os.path.isabs(path) or
97
            path in _forbidden or
98
            filter(lambda p: utils.IsBelowDir(p, path), _forbidden))
99

    
100
  return utils.NiceSort(filter(_Check, map(os.path.normpath, paths)))
101

    
102

    
103
def ComputeWrongFileStoragePaths(_filename=pathutils.FILE_STORAGE_PATHS_FILE):
104
  """Returns a list of file storage paths whose prefix is considered bad.
105

106
  See L{_ComputeWrongFileStoragePaths}.
107

108
  """
109
  return _ComputeWrongFileStoragePaths(_LoadAllowedFileStoragePaths(_filename))
110

    
111

    
112
def _CheckFileStoragePath(path, allowed):
113
  """Checks if a path is in a list of allowed paths for file storage.
114

115
  @type path: string
116
  @param path: Path to check
117
  @type allowed: list
118
  @param allowed: List of allowed paths
119
  @raise errors.FileStoragePathError: If the path is not allowed
120

121
  """
122
  if not os.path.isabs(path):
123
    raise errors.FileStoragePathError("File storage path must be absolute,"
124
                                      " got '%s'" % path)
125

    
126
  for i in allowed:
127
    if not os.path.isabs(i):
128
      logging.info("Ignoring relative path '%s' for file storage", i)
129
      continue
130

    
131
    if utils.IsBelowDir(i, path):
132
      break
133
  else:
134
    raise errors.FileStoragePathError("Path '%s' is not acceptable for file"
135
                                      " storage" % path)
136

    
137

    
138
def _LoadAllowedFileStoragePaths(filename):
139
  """Loads file containing allowed file storage paths.
140

141
  @rtype: list
142
  @return: List of allowed paths (can be an empty list)
143

144
  """
145
  try:
146
    contents = utils.ReadFile(filename)
147
  except EnvironmentError:
148
    return []
149
  else:
150
    return utils.FilterEmptyLinesAndComments(contents)
151

    
152

    
153
def CheckFileStoragePath(path, _filename=pathutils.FILE_STORAGE_PATHS_FILE):
154
  """Checks if a path is allowed for file storage.
155

156
  @type path: string
157
  @param path: Path to check
158
  @raise errors.FileStoragePathError: If the path is not allowed
159

160
  """
161
  allowed = _LoadAllowedFileStoragePaths(_filename)
162

    
163
  if _ComputeWrongFileStoragePaths([path]):
164
    raise errors.FileStoragePathError("Path '%s' uses a forbidden prefix" %
165
                                      path)
166

    
167
  _CheckFileStoragePath(path, allowed)
168

    
169

    
170
class LogicalVolume(base.BlockDev):
171
  """Logical Volume block device.
172

173
  """
174
  _VALID_NAME_RE = re.compile("^[a-zA-Z0-9+_.-]*$")
175
  _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=k", "--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
    # pe_size is in KB
694
    amount *= 1024
695
    rest = amount % full_stripe_size
696
    if rest != 0:
697
      amount += full_stripe_size - rest
698
    cmd = ["lvextend", "-L", "+%dk" % amount]
699
    if dryrun:
700
      cmd.append("--test")
701
    # we try multiple algorithms since the 'best' ones might not have
702
    # space available in the right place, but later ones might (since
703
    # they have less constraints); also note that only recent LVM
704
    # supports 'cling'
705
    for alloc_policy in "contiguous", "cling", "normal":
706
      result = utils.RunCmd(cmd + ["--alloc", alloc_policy, self.dev_path])
707
      if not result.failed:
708
        return
709
    base.ThrowError("Can't grow LV %s: %s", self.dev_path, result.output)
710

    
711

    
712
class FileStorage(base.BlockDev):
713
  """File device.
714

715
  This class represents the a file storage backend device.
716

717
  The unique_id for the file device is a (file_driver, file_path) tuple.
718

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

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

    
732
    CheckFileStoragePath(self.dev_path)
733

    
734
    self.Attach()
735

    
736
  def Assemble(self):
737
    """Assemble the device.
738

739
    Checks whether the file device exists, raises BlockDeviceError otherwise.
740

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

    
745
  def Shutdown(self):
746
    """Shutdown the device.
747

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

751
    """
752
    pass
753

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

757
    This is a no-op for the file type.
758

759
    """
760
    pass
761

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

765
    This is a no-op for the file type.
766

767
    """
768
    pass
769

    
770
  def Remove(self):
771
    """Remove the file backing the block device.
772

773
    @rtype: boolean
774
    @return: True if the removal was successful
775

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

    
783
  def Rename(self, new_id):
784
    """Renames the file.
785

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

    
790
  def Grow(self, amount, dryrun, backingstore):
791
    """Grow the file
792

793
    @param amount: the amount (in mebibytes) to grow with
794

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

    
813
  def Attach(self):
814
    """Attach to an existing file.
815

816
    Check if this file already exists.
817

818
    @rtype: boolean
819
    @return: True if file exists
820

821
    """
822
    self.attached = os.path.exists(self.dev_path)
823
    return self.attached
824

    
825
  def GetActualSize(self):
826
    """Return the actual disk size.
827

828
    @note: the device needs to be active when this is called
829

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

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

842
    @param size: the size of file in MiB
843

844
    @rtype: L{bdev.FileStorage}
845
    @return: an instance of FileStorage
846

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

    
854
    dev_path = unique_id[1]
855

    
856
    CheckFileStoragePath(dev_path)
857

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

    
868
    return FileStorage(unique_id, children, size, params)
869

    
870

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

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

878
  For the time being, pathnames are required to lie under /dev.
879

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

884
    The unique_id is a path under /dev.
885

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

    
903
    self.major = self.minor = None
904
    self.Attach()
905

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

910
    This is a noop, we only return a PersistentBlockDevice instance
911

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

    
918
  def Remove(self):
919
    """Remove a device
920

921
    This is a noop
922

923
    """
924
    pass
925

    
926
  def Rename(self, new_id):
927
    """Rename this device.
928

929
    """
930
    base.ThrowError("Rename is not supported for PersistentBlockDev storage")
931

    
932
  def Attach(self):
933
    """Attach to an existing block device.
934

935

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

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

    
948
    self.major = os.major(st.st_rdev)
949
    self.minor = os.minor(st.st_rdev)
950
    self.attached = True
951

    
952
    return True
953

    
954
  def Assemble(self):
955
    """Assemble the device.
956

957
    """
958
    pass
959

    
960
  def Shutdown(self):
961
    """Shutdown the device.
962

963
    """
964
    pass
965

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

969
    """
970
    pass
971

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

975
    """
976
    pass
977

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

981
    """
982
    base.ThrowError("Grow is not supported for PersistentBlockDev storage")
983

    
984

    
985
class RADOSBlockDevice(base.BlockDev):
986
  """A RADOS Block Device (rbd).
987

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

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

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

    
1001
    self.driver, self.rbd_name = unique_id
1002

    
1003
    self.major = self.minor = None
1004
    self.Attach()
1005

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

1010
    Provision a new rbd volume inside a RADOS pool.
1011

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

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

    
1030
    return RADOSBlockDevice(unique_id, children, size, params)
1031

    
1032
  def Remove(self):
1033
    """Remove the rbd device.
1034

1035
    """
1036
    rbd_pool = self.params[constants.LDP_POOL]
1037
    rbd_name = self.unique_id[1]
1038

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

    
1043
    # First shutdown the device (remove mappings).
1044
    self.Shutdown()
1045

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

    
1053
  def Rename(self, new_id):
1054
    """Rename this device.
1055

1056
    """
1057
    pass
1058

    
1059
  def Attach(self):
1060
    """Attach to an existing rbd device.
1061

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

1065
    """
1066
    self.attached = False
1067

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

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

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

    
1081
    self.major = os.major(st.st_rdev)
1082
    self.minor = os.minor(st.st_rdev)
1083
    self.attached = True
1084

    
1085
    return True
1086

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

1090
    This method should be idempotent if the mapping already exists.
1091

1092
    @rtype: string
1093
    @return: the block device path that corresponds to the volume
1094

1095
    """
1096
    pool = self.params[constants.LDP_POOL]
1097
    name = unique_id[1]
1098

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

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

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

    
1118
    # The device was successfully mapped. Return it.
1119
    return rbd_dev
1120

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

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

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

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

    
1161
      return cls._ParseRbdShowmappedPlain(result.output, volume_name)
1162

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

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

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

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

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

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

    
1194
        rbd_dev = d["device"]
1195

    
1196
    return rbd_dev
1197

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

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

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

1213
    """
1214
    allfields = 5
1215
    volumefield = 2
1216
    devicefield = 4
1217

    
1218
    lines = output.splitlines()
1219

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

    
1223
    # Check for empty output.
1224
    if not splitted_lines:
1225
      return None
1226

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

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

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

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

    
1249
    # The given volume is not mapped.
1250
    return None
1251

    
1252
  def Assemble(self):
1253
    """Assemble the device.
1254

1255
    """
1256
    pass
1257

    
1258
  def Shutdown(self):
1259
    """Shutdown the device.
1260

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

    
1266
    # Unmap the block device from the Volume.
1267
    self._UnmapVolumeFromBlockdev(self.unique_id)
1268

    
1269
    self.minor = None
1270
    self.dev_path = None
1271

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

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

1278
    """
1279
    pool = self.params[constants.LDP_POOL]
1280
    name = unique_id[1]
1281

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

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

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

1296
    """
1297
    pass
1298

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

1302
    """
1303
    pass
1304

    
1305
  def Grow(self, amount, dryrun, backingstore):
1306
    """Grow the Volume.
1307

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

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

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

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

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

    
1338

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

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

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

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

    
1354
    self.driver, self.vol_name = unique_id
1355
    self.ext_params = params
1356

    
1357
    self.major = self.minor = None
1358
    self.Attach()
1359

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

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

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

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

    
1380
    return ExtStorageDevice(unique_id, children, size, params)
1381

    
1382
  def Remove(self):
1383
    """Remove the extstorage device.
1384

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

    
1390
    # First shutdown the device (remove mappings).
1391
    self.Shutdown()
1392

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

    
1398
  def Rename(self, new_id):
1399
    """Rename this device.
1400

1401
    """
1402
    pass
1403

    
1404
  def Attach(self):
1405
    """Attach to an existing extstorage device.
1406

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

1410
    """
1411
    self.attached = False
1412

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

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

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

    
1428
    self.major = os.major(st.st_rdev)
1429
    self.minor = os.minor(st.st_rdev)
1430
    self.attached = True
1431

    
1432
    return True
1433

    
1434
  def Assemble(self):
1435
    """Assemble the device.
1436

1437
    """
1438
    pass
1439

    
1440
  def Shutdown(self):
1441
    """Shutdown the device.
1442

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

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

    
1453
    self.minor = None
1454
    self.dev_path = None
1455

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

1459
    """
1460
    pass
1461

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

1465
    """
1466
    pass
1467

    
1468
  def Grow(self, amount, dryrun, backingstore):
1469
    """Grow the Volume.
1470

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

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

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

    
1487
    new_size = self.size + amount
1488

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

    
1494
  def SetInfo(self, text):
1495
    """Update metadata with info text.
1496

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

    
1502
    # Only up to 128 characters are allowed
1503
    text = text[:128]
1504

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

    
1510

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

1515
  Take an External Storage action concerning or affecting
1516
  a specific Volume inside the External Storage.
1517

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

1534
  """
1535
  driver, vol_name = unique_id
1536

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

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

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

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

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

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

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

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

    
1583
  if action == constants.ES_ACTION_ATTACH:
1584
    return result.stdout
1585

    
1586

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

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

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

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

    
1606
  es_dir = utils.FindFile(name, es_base_dir, os.path.isdir)
1607

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

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

    
1617
  es_files[constants.ES_PARAMETERS_FILE] = True
1618

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

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

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

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

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

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

    
1659

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

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

1677
  """
1678
  vol_name = unique_id[1]
1679

    
1680
  result = {}
1681
  result["VOL_NAME"] = vol_name
1682

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

    
1687
  if size is not None:
1688
    result["VOL_SIZE"] = size
1689

    
1690
  if grow is not None:
1691
    result["VOL_NEW_SIZE"] = grow
1692

    
1693
  if metadata is not None:
1694
    result["VOL_METADATA"] = metadata
1695

    
1696
  return result
1697

    
1698

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

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

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

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

    
1719

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

    
1728
if constants.ENABLE_FILE_STORAGE or constants.ENABLE_SHARED_FILE_STORAGE:
1729
  DEV_MAP[constants.LD_FILE] = FileStorage
1730

    
1731

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

    
1736

    
1737
def _VerifyDiskParams(disk):
1738
  """Verifies if all disk parameters are set.
1739

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

    
1746

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

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

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

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

    
1767

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

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

1774
  @type disk: L{objects.Disk}
1775
  @param disk: the disk object to assemble
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
  _VerifyDiskParams(disk)
1783
  device = DEV_MAP[disk.dev_type](disk.physical_id, children, disk.size,
1784
                                  disk.params)
1785
  device.Assemble()
1786
  return device
1787

    
1788

    
1789
def Create(disk, children, excl_stor):
1790
  """Create a device.
1791

1792
  @type disk: L{objects.Disk}
1793
  @param disk: the disk object to create
1794
  @type children: list of L{bdev.BlockDev}
1795
  @param children: the list of block devices that are children of the device
1796
                  represented by the disk parameter
1797
  @type excl_stor: boolean
1798
  @param excl_stor: Whether exclusive_storage is active
1799
  @rtype: L{bdev.BlockDev}
1800
  @return: the created device, or C{None} in case of an error
1801

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