Statistics
| Branch: | Tag: | Revision:

root / lib / storage / bdev.py @ 544e5b6c

History | View | Annotate | Download (58.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 base
39
from ganeti.storage import drbd
40
from ganeti.storage import filestorage
41

    
42

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

46
  """
47
  pass
48

    
49

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

53
  @param result: result from RunCmd
54

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

    
60

    
61
class LogicalVolume(base.BlockDev):
62
  """Logical Volume block device.
63

64
  """
65
  _VALID_NAME_RE = re.compile("^[a-zA-Z0-9+_.-]*$")
66
  _PARSE_PV_DEV_RE = re.compile(r"^([^ ()]+)\([0-9]+\)$")
67
  _INVALID_NAMES = compat.UniqueFrozenset([".", "..", "snapshot", "pvmove"])
68
  _INVALID_SUBSTRINGS = compat.UniqueFrozenset(["_mlog", "_mimage"])
69

    
70
  def __init__(self, unique_id, children, size, params, dyn_params, *args):
71
    """Attaches to a LV device.
72

73
    The unique_id is a tuple (vg_name, lv_name)
74

75
    """
76
    super(LogicalVolume, self).__init__(unique_id, children, size, params,
77
                                        dyn_params, *args)
78
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
79
      raise ValueError("Invalid configuration data %s" % str(unique_id))
80
    self._vg_name, self._lv_name = unique_id
81
    self._ValidateName(self._vg_name)
82
    self._ValidateName(self._lv_name)
83
    self.dev_path = utils.PathJoin("/dev", self._vg_name, self._lv_name)
84
    self._degraded = True
85
    self.major = self.minor = self.pe_size = self.stripe_count = None
86
    self.pv_names = None
87
    self.Attach()
88

    
89
  @staticmethod
90
  def _GetStdPvSize(pvs_info):
91
    """Return the the standard PV size (used with exclusive storage).
92

93
    @param pvs_info: list of objects.LvmPvInfo, cannot be empty
94
    @rtype: float
95
    @return: size in MiB
96

97
    """
98
    assert len(pvs_info) > 0
99
    smallest = min([pv.size for pv in pvs_info])
100
    return smallest / (1 + constants.PART_MARGIN + constants.PART_RESERVED)
101

    
102
  @staticmethod
103
  def _ComputeNumPvs(size, pvs_info):
104
    """Compute the number of PVs needed for an LV (with exclusive storage).
105

106
    @type size: float
107
    @param size: LV size in MiB
108
    @param pvs_info: list of objects.LvmPvInfo, cannot be empty
109
    @rtype: integer
110
    @return: number of PVs needed
111
    """
112
    assert len(pvs_info) > 0
113
    pv_size = float(LogicalVolume._GetStdPvSize(pvs_info))
114
    return int(math.ceil(float(size) / pv_size))
115

    
116
  @staticmethod
117
  def _GetEmptyPvNames(pvs_info, max_pvs=None):
118
    """Return a list of empty PVs, by name.
119

120
    """
121
    empty_pvs = filter(objects.LvmPvInfo.IsEmpty, pvs_info)
122
    if max_pvs is not None:
123
      empty_pvs = empty_pvs[:max_pvs]
124
    return map((lambda pv: pv.name), empty_pvs)
125

    
126
  @classmethod
127
  def Create(cls, unique_id, children, size, spindles, params, excl_stor,
128
             dyn_params):
129
    """Create a new logical volume.
130

131
    """
132
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
133
      raise errors.ProgrammerError("Invalid configuration data %s" %
134
                                   str(unique_id))
135
    vg_name, lv_name = unique_id
136
    cls._ValidateName(vg_name)
137
    cls._ValidateName(lv_name)
138
    pvs_info = cls.GetPVInfo([vg_name])
139
    if not pvs_info:
140
      if excl_stor:
141
        msg = "No (empty) PVs found"
142
      else:
143
        msg = "Can't compute PV info for vg %s" % vg_name
144
      base.ThrowError(msg)
145
    pvs_info.sort(key=(lambda pv: pv.free), reverse=True)
146

    
147
    pvlist = [pv.name for pv in pvs_info]
148
    if compat.any(":" in v for v in pvlist):
149
      base.ThrowError("Some of your PVs have the invalid character ':' in their"
150
                      " name, this is not supported - please filter them out"
151
                      " in lvm.conf using either 'filter' or 'preferred_names'")
152

    
153
    current_pvs = len(pvlist)
154
    desired_stripes = params[constants.LDP_STRIPES]
155
    stripes = min(current_pvs, desired_stripes)
156

    
157
    if excl_stor:
158
      if spindles is None:
159
        base.ThrowError("Unspecified number of spindles: this is required"
160
                        "when exclusive storage is enabled, try running"
161
                        " gnt-cluster repair-disk-sizes")
162
      (err_msgs, _) = utils.LvmExclusiveCheckNodePvs(pvs_info)
163
      if err_msgs:
164
        for m in err_msgs:
165
          logging.warning(m)
166
      req_pvs = cls._ComputeNumPvs(size, pvs_info)
167
      if spindles < req_pvs:
168
        base.ThrowError("Requested number of spindles (%s) is not enough for"
169
                        " a disk of %d MB (at least %d spindles needed)",
170
                        spindles, size, req_pvs)
171
      else:
172
        req_pvs = spindles
173
      pvlist = cls._GetEmptyPvNames(pvs_info, req_pvs)
174
      current_pvs = len(pvlist)
175
      if current_pvs < req_pvs:
176
        base.ThrowError("Not enough empty PVs (spindles) to create a disk of %d"
177
                        " MB: %d available, %d needed",
178
                        size, current_pvs, req_pvs)
179
      assert current_pvs == len(pvlist)
180
      # We must update stripes to be sure to use all the desired spindles
181
      stripes = current_pvs
182
      if stripes > desired_stripes:
183
        # Don't warn when lowering stripes, as it's no surprise
184
        logging.warning("Using %s stripes instead of %s, to be able to use"
185
                        " %s spindles", stripes, desired_stripes, current_pvs)
186

    
187
    else:
188
      if stripes < desired_stripes:
189
        logging.warning("Could not use %d stripes for VG %s, as only %d PVs are"
190
                        " available.", desired_stripes, vg_name, current_pvs)
191
      free_size = sum([pv.free for pv in pvs_info])
192
      # The size constraint should have been checked from the master before
193
      # calling the create function.
194
      if free_size < size:
195
        base.ThrowError("Not enough free space: required %s,"
196
                        " available %s", size, free_size)
197

    
198
    # If the free space is not well distributed, we won't be able to
199
    # create an optimally-striped volume; in that case, we want to try
200
    # with N, N-1, ..., 2, and finally 1 (non-stripped) number of
201
    # stripes
202
    cmd = ["lvcreate", "-L%dm" % size, "-n%s" % lv_name]
203
    for stripes_arg in range(stripes, 0, -1):
204
      result = utils.RunCmd(cmd + ["-i%d" % stripes_arg] + [vg_name] + pvlist)
205
      if not result.failed:
206
        break
207
    if result.failed:
208
      base.ThrowError("LV create failed (%s): %s",
209
                      result.fail_reason, result.output)
210
    return LogicalVolume(unique_id, children, size, params, dyn_params)
211

    
212
  @staticmethod
213
  def _GetVolumeInfo(lvm_cmd, fields):
214
    """Returns LVM Volume infos using lvm_cmd
215

216
    @param lvm_cmd: Should be one of "pvs", "vgs" or "lvs"
217
    @param fields: Fields to return
218
    @return: A list of dicts each with the parsed fields
219

220
    """
221
    if not fields:
222
      raise errors.ProgrammerError("No fields specified")
223

    
224
    sep = "|"
225
    cmd = [lvm_cmd, "--noheadings", "--nosuffix", "--units=m", "--unbuffered",
226
           "--separator=%s" % sep, "-o%s" % ",".join(fields)]
227

    
228
    result = utils.RunCmd(cmd)
229
    if result.failed:
230
      raise errors.CommandError("Can't get the volume information: %s - %s" %
231
                                (result.fail_reason, result.output))
232

    
233
    data = []
234
    for line in result.stdout.splitlines():
235
      splitted_fields = line.strip().split(sep)
236

    
237
      if len(fields) != len(splitted_fields):
238
        raise errors.CommandError("Can't parse %s output: line '%s'" %
239
                                  (lvm_cmd, line))
240

    
241
      data.append(splitted_fields)
242

    
243
    return data
244

    
245
  @classmethod
246
  def GetPVInfo(cls, vg_names, filter_allocatable=True, include_lvs=False):
247
    """Get the free space info for PVs in a volume group.
248

249
    @param vg_names: list of volume group names, if empty all will be returned
250
    @param filter_allocatable: whether to skip over unallocatable PVs
251
    @param include_lvs: whether to include a list of LVs hosted on each PV
252

253
    @rtype: list
254
    @return: list of objects.LvmPvInfo objects
255

256
    """
257
    # We request "lv_name" field only if we care about LVs, so we don't get
258
    # a long list of entries with many duplicates unless we really have to.
259
    # The duplicate "pv_name" field will be ignored.
260
    if include_lvs:
261
      lvfield = "lv_name"
262
    else:
263
      lvfield = "pv_name"
264
    try:
265
      info = cls._GetVolumeInfo("pvs", ["pv_name", "vg_name", "pv_free",
266
                                        "pv_attr", "pv_size", lvfield])
267
    except errors.GenericError, err:
268
      logging.error("Can't get PV information: %s", err)
269
      return None
270

    
271
    # When asked for LVs, "pvs" may return multiple entries for the same PV-LV
272
    # pair. We sort entries by PV name and then LV name, so it's easy to weed
273
    # out duplicates.
274
    if include_lvs:
275
      info.sort(key=(lambda i: (i[0], i[5])))
276
    data = []
277
    lastpvi = None
278
    for (pv_name, vg_name, pv_free, pv_attr, pv_size, lv_name) in info:
279
      # (possibly) skip over pvs which are not allocatable
280
      if filter_allocatable and pv_attr[0] != "a":
281
        continue
282
      # (possibly) skip over pvs which are not in the right volume group(s)
283
      if vg_names and vg_name not in vg_names:
284
        continue
285
      # Beware of duplicates (check before inserting)
286
      if lastpvi and lastpvi.name == pv_name:
287
        if include_lvs and lv_name:
288
          if not lastpvi.lv_list or lastpvi.lv_list[-1] != lv_name:
289
            lastpvi.lv_list.append(lv_name)
290
      else:
291
        if include_lvs and lv_name:
292
          lvl = [lv_name]
293
        else:
294
          lvl = []
295
        lastpvi = objects.LvmPvInfo(name=pv_name, vg_name=vg_name,
296
                                    size=float(pv_size), free=float(pv_free),
297
                                    attributes=pv_attr, lv_list=lvl)
298
        data.append(lastpvi)
299

    
300
    return data
301

    
302
  @classmethod
303
  def _GetRawFreePvInfo(cls, vg_name):
304
    """Return info (size/free) about PVs.
305

306
    @type vg_name: string
307
    @param vg_name: VG name
308
    @rtype: tuple
309
    @return: (standard_pv_size_in_MiB, number_of_free_pvs, total_number_of_pvs)
310

311
    """
312
    pvs_info = cls.GetPVInfo([vg_name])
313
    if not pvs_info:
314
      pv_size = 0.0
315
      free_pvs = 0
316
      num_pvs = 0
317
    else:
318
      pv_size = cls._GetStdPvSize(pvs_info)
319
      free_pvs = len(cls._GetEmptyPvNames(pvs_info))
320
      num_pvs = len(pvs_info)
321
    return (pv_size, free_pvs, num_pvs)
322

    
323
  @classmethod
324
  def _GetExclusiveStorageVgFree(cls, vg_name):
325
    """Return the free disk space in the given VG, in exclusive storage mode.
326

327
    @type vg_name: string
328
    @param vg_name: VG name
329
    @rtype: float
330
    @return: free space in MiB
331
    """
332
    (pv_size, free_pvs, _) = cls._GetRawFreePvInfo(vg_name)
333
    return pv_size * free_pvs
334

    
335
  @classmethod
336
  def GetVgSpindlesInfo(cls, vg_name):
337
    """Get the free space info for specific VGs.
338

339
    @param vg_name: volume group name
340
    @rtype: tuple
341
    @return: (free_spindles, total_spindles)
342

343
    """
344
    (_, free_pvs, num_pvs) = cls._GetRawFreePvInfo(vg_name)
345
    return (free_pvs, num_pvs)
346

    
347
  @classmethod
348
  def GetVGInfo(cls, vg_names, excl_stor, filter_readonly=True):
349
    """Get the free space info for specific VGs.
350

351
    @param vg_names: list of volume group names, if empty all will be returned
352
    @param excl_stor: whether exclusive_storage is enabled
353
    @param filter_readonly: whether to skip over readonly VGs
354

355
    @rtype: list
356
    @return: list of tuples (free_space, total_size, name) with free_space in
357
             MiB
358

359
    """
360
    try:
361
      info = cls._GetVolumeInfo("vgs", ["vg_name", "vg_free", "vg_attr",
362
                                        "vg_size"])
363
    except errors.GenericError, err:
364
      logging.error("Can't get VG information: %s", err)
365
      return None
366

    
367
    data = []
368
    for vg_name, vg_free, vg_attr, vg_size in info:
369
      # (possibly) skip over vgs which are not writable
370
      if filter_readonly and vg_attr[0] == "r":
371
        continue
372
      # (possibly) skip over vgs which are not in the right volume group(s)
373
      if vg_names and vg_name not in vg_names:
374
        continue
375
      # Exclusive storage needs a different concept of free space
376
      if excl_stor:
377
        es_free = cls._GetExclusiveStorageVgFree(vg_name)
378
        assert es_free <= vg_free
379
        vg_free = es_free
380
      data.append((float(vg_free), float(vg_size), vg_name))
381

    
382
    return data
383

    
384
  @classmethod
385
  def _ValidateName(cls, name):
386
    """Validates that a given name is valid as VG or LV name.
387

388
    The list of valid characters and restricted names is taken out of
389
    the lvm(8) manpage, with the simplification that we enforce both
390
    VG and LV restrictions on the names.
391

392
    """
393
    if (not cls._VALID_NAME_RE.match(name) or
394
        name in cls._INVALID_NAMES or
395
        compat.any(substring in name for substring in cls._INVALID_SUBSTRINGS)):
396
      base.ThrowError("Invalid LVM name '%s'", name)
397

    
398
  def Remove(self):
399
    """Remove this logical volume.
400

401
    """
402
    if not self.minor and not self.Attach():
403
      # the LV does not exist
404
      return
405
    result = utils.RunCmd(["lvremove", "-f", "%s/%s" %
406
                           (self._vg_name, self._lv_name)])
407
    if result.failed:
408
      base.ThrowError("Can't lvremove: %s - %s",
409
                      result.fail_reason, result.output)
410

    
411
  def Rename(self, new_id):
412
    """Rename this logical volume.
413

414
    """
415
    if not isinstance(new_id, (tuple, list)) or len(new_id) != 2:
416
      raise errors.ProgrammerError("Invalid new logical id '%s'" % new_id)
417
    new_vg, new_name = new_id
418
    if new_vg != self._vg_name:
419
      raise errors.ProgrammerError("Can't move a logical volume across"
420
                                   " volume groups (from %s to to %s)" %
421
                                   (self._vg_name, new_vg))
422
    result = utils.RunCmd(["lvrename", new_vg, self._lv_name, new_name])
423
    if result.failed:
424
      base.ThrowError("Failed to rename the logical volume: %s", result.output)
425
    self._lv_name = new_name
426
    self.dev_path = utils.PathJoin("/dev", self._vg_name, self._lv_name)
427

    
428
  @classmethod
429
  def _ParseLvInfoLine(cls, line, sep):
430
    """Parse one line of the lvs output used in L{_GetLvInfo}.
431

432
    """
433
    elems = line.strip().rstrip(sep).split(sep)
434
    if len(elems) != 6:
435
      base.ThrowError("Can't parse LVS output, len(%s) != 6", str(elems))
436

    
437
    (status, major, minor, pe_size, stripes, pvs) = elems
438
    if len(status) < 6:
439
      base.ThrowError("lvs lv_attr is not at least 6 characters (%s)", status)
440

    
441
    try:
442
      major = int(major)
443
      minor = int(minor)
444
    except (TypeError, ValueError), err:
445
      base.ThrowError("lvs major/minor cannot be parsed: %s", str(err))
446

    
447
    try:
448
      pe_size = int(float(pe_size))
449
    except (TypeError, ValueError), err:
450
      base.ThrowError("Can't parse vg extent size: %s", err)
451

    
452
    try:
453
      stripes = int(stripes)
454
    except (TypeError, ValueError), err:
455
      base.ThrowError("Can't parse the number of stripes: %s", err)
456

    
457
    pv_names = []
458
    for pv in pvs.split(","):
459
      m = re.match(cls._PARSE_PV_DEV_RE, pv)
460
      if not m:
461
        base.ThrowError("Can't parse this device list: %s", pvs)
462
      pv_names.append(m.group(1))
463
    assert len(pv_names) > 0
464

    
465
    return (status, major, minor, pe_size, stripes, pv_names)
466

    
467
  @classmethod
468
  def _GetLvInfo(cls, dev_path, _run_cmd=utils.RunCmd):
469
    """Get info about the given existing LV to be used.
470

471
    """
472
    sep = "|"
473
    result = _run_cmd(["lvs", "--noheadings", "--separator=%s" % sep,
474
                       "--units=k", "--nosuffix",
475
                       "-olv_attr,lv_kernel_major,lv_kernel_minor,"
476
                       "vg_extent_size,stripes,devices", dev_path])
477
    if result.failed:
478
      base.ThrowError("Can't find LV %s: %s, %s",
479
                      dev_path, result.fail_reason, result.output)
480
    # the output can (and will) have multiple lines for multi-segment
481
    # LVs, as the 'stripes' parameter is a segment one, so we take
482
    # only the last entry, which is the one we're interested in; note
483
    # that with LVM2 anyway the 'stripes' value must be constant
484
    # across segments, so this is a no-op actually
485
    out = result.stdout.splitlines()
486
    if not out: # totally empty result? splitlines() returns at least
487
                # one line for any non-empty string
488
      base.ThrowError("Can't parse LVS output, no lines? Got '%s'", str(out))
489
    pv_names = set()
490
    for line in out:
491
      (status, major, minor, pe_size, stripes, more_pvs) = \
492
        cls._ParseLvInfoLine(line, sep)
493
      pv_names.update(more_pvs)
494
    return (status, major, minor, pe_size, stripes, pv_names)
495

    
496
  def Attach(self):
497
    """Attach to an existing LV.
498

499
    This method will try to see if an existing and active LV exists
500
    which matches our name. If so, its major/minor will be
501
    recorded.
502

503
    """
504
    self.attached = False
505
    try:
506
      (status, major, minor, pe_size, stripes, pv_names) = \
507
        self._GetLvInfo(self.dev_path)
508
    except errors.BlockDeviceError:
509
      return False
510

    
511
    self.major = major
512
    self.minor = minor
513
    self.pe_size = pe_size
514
    self.stripe_count = stripes
515
    self._degraded = status[0] == "v" # virtual volume, i.e. doesn't backing
516
                                      # storage
517
    self.pv_names = pv_names
518
    self.attached = True
519
    return True
520

    
521
  def Assemble(self):
522
    """Assemble the device.
523

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

528
    """
529
    result = utils.RunCmd(["lvchange", "-ay", self.dev_path])
530
    if result.failed:
531
      base.ThrowError("Can't activate lv %s: %s", self.dev_path, result.output)
532

    
533
  def Shutdown(self):
534
    """Shutdown the device.
535

536
    This is a no-op for the LV device type, as we don't deactivate the
537
    volumes on shutdown.
538

539
    """
540
    pass
541

    
542
  def GetSyncStatus(self):
543
    """Returns the sync status of the device.
544

545
    If this device is a mirroring device, this function returns the
546
    status of the mirror.
547

548
    For logical volumes, sync_percent and estimated_time are always
549
    None (no recovery in progress, as we don't handle the mirrored LV
550
    case). The is_degraded parameter is the inverse of the ldisk
551
    parameter.
552

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

559
    The status was already read in Attach, so we just return it.
560

561
    @rtype: objects.BlockDevStatus
562

563
    """
564
    if self._degraded:
565
      ldisk_status = constants.LDS_FAULTY
566
    else:
567
      ldisk_status = constants.LDS_OKAY
568

    
569
    return objects.BlockDevStatus(dev_path=self.dev_path,
570
                                  major=self.major,
571
                                  minor=self.minor,
572
                                  sync_percent=None,
573
                                  estimated_time=None,
574
                                  is_degraded=self._degraded,
575
                                  ldisk_status=ldisk_status)
576

    
577
  def Open(self, force=False):
578
    """Make the device ready for I/O.
579

580
    This is a no-op for the LV device type.
581

582
    """
583
    pass
584

    
585
  def Close(self):
586
    """Notifies that the device will no longer be used for I/O.
587

588
    This is a no-op for the LV device type.
589

590
    """
591
    pass
592

    
593
  def Snapshot(self, size):
594
    """Create a snapshot copy of an lvm block device.
595

596
    @returns: tuple (vg, lv)
597

598
    """
599
    snap_name = self._lv_name + ".snap"
600

    
601
    # remove existing snapshot if found
602
    snap = LogicalVolume((self._vg_name, snap_name), None, size, self.params,
603
                         self.dyn_params)
604
    base.IgnoreError(snap.Remove)
605

    
606
    vg_info = self.GetVGInfo([self._vg_name], False)
607
    if not vg_info:
608
      base.ThrowError("Can't compute VG info for vg %s", self._vg_name)
609
    free_size, _, _ = vg_info[0]
610
    if free_size < size:
611
      base.ThrowError("Not enough free space: required %s,"
612
                      " available %s", size, free_size)
613

    
614
    _CheckResult(utils.RunCmd(["lvcreate", "-L%dm" % size, "-s",
615
                               "-n%s" % snap_name, self.dev_path]))
616

    
617
    return (self._vg_name, snap_name)
618

    
619
  def _RemoveOldInfo(self):
620
    """Try to remove old tags from the lv.
621

622
    """
623
    result = utils.RunCmd(["lvs", "-o", "tags", "--noheadings", "--nosuffix",
624
                           self.dev_path])
625
    _CheckResult(result)
626

    
627
    raw_tags = result.stdout.strip()
628
    if raw_tags:
629
      for tag in raw_tags.split(","):
630
        _CheckResult(utils.RunCmd(["lvchange", "--deltag",
631
                                   tag.strip(), self.dev_path]))
632

    
633
  def SetInfo(self, text):
634
    """Update metadata with info text.
635

636
    """
637
    base.BlockDev.SetInfo(self, text)
638

    
639
    self._RemoveOldInfo()
640

    
641
    # Replace invalid characters
642
    text = re.sub("^[^A-Za-z0-9_+.]", "_", text)
643
    text = re.sub("[^-A-Za-z0-9_+.]", "_", text)
644

    
645
    # Only up to 128 characters are allowed
646
    text = text[:128]
647

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

    
650
  def _GetGrowthAvaliabilityExclStor(self):
651
    """Return how much the disk can grow with exclusive storage.
652

653
    @rtype: float
654
    @return: available space in Mib
655

656
    """
657
    pvs_info = self.GetPVInfo([self._vg_name])
658
    if not pvs_info:
659
      base.ThrowError("Cannot get information about PVs for %s", self.dev_path)
660
    std_pv_size = self._GetStdPvSize(pvs_info)
661
    free_space = sum(pvi.free - (pvi.size - std_pv_size)
662
                        for pvi in pvs_info
663
                        if pvi.name in self.pv_names)
664
    return free_space
665

    
666
  def Grow(self, amount, dryrun, backingstore, excl_stor):
667
    """Grow the logical volume.
668

669
    """
670
    if not backingstore:
671
      return
672
    if self.pe_size is None or self.stripe_count is None:
673
      if not self.Attach():
674
        base.ThrowError("Can't attach to LV during Grow()")
675
    full_stripe_size = self.pe_size * self.stripe_count
676
    # pe_size is in KB
677
    amount *= 1024
678
    rest = amount % full_stripe_size
679
    if rest != 0:
680
      amount += full_stripe_size - rest
681
    cmd = ["lvextend", "-L", "+%dk" % amount]
682
    if dryrun:
683
      cmd.append("--test")
684
    if excl_stor:
685
      free_space = self._GetGrowthAvaliabilityExclStor()
686
      # amount is in KiB, free_space in MiB
687
      if amount > free_space * 1024:
688
        base.ThrowError("Not enough free space to grow %s: %d MiB required,"
689
                        " %d available", self.dev_path, amount / 1024,
690
                        free_space)
691
      # Disk growth doesn't grow the number of spindles, so we must stay within
692
      # our assigned volumes
693
      pvlist = list(self.pv_names)
694
    else:
695
      pvlist = []
696
    # we try multiple algorithms since the 'best' ones might not have
697
    # space available in the right place, but later ones might (since
698
    # they have less constraints); also note that only recent LVM
699
    # supports 'cling'
700
    for alloc_policy in "contiguous", "cling", "normal":
701
      result = utils.RunCmd(cmd + ["--alloc", alloc_policy, self.dev_path] +
702
                            pvlist)
703
      if not result.failed:
704
        return
705
    base.ThrowError("Can't grow LV %s: %s", self.dev_path, result.output)
706

    
707
  def GetActualSpindles(self):
708
    """Return the number of spindles used.
709

710
    """
711
    assert self.attached, "BlockDevice not attached in GetActualSpindles()"
712
    return len(self.pv_names)
713

    
714

    
715
class FileStorage(base.BlockDev):
716
  """File device.
717

718
  This class represents a file storage backend device.
719

720
  The unique_id for the file device is a (file_driver, file_path) tuple.
721

722
  """
723
  def __init__(self, unique_id, children, size, params, dyn_params, *args):
724
    """Initalizes a file device backend.
725

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

    
736
    filestorage.CheckFileStoragePathAcceptance(self.dev_path)
737

    
738
    self.Attach()
739

    
740
  def Assemble(self):
741
    """Assemble the device.
742

743
    Checks whether the file device exists, raises BlockDeviceError otherwise.
744

745
    """
746
    if not os.path.exists(self.dev_path):
747
      base.ThrowError("File device '%s' does not exist" % self.dev_path)
748

    
749
  def Shutdown(self):
750
    """Shutdown the device.
751

752
    This is a no-op for the file type, as we don't deactivate
753
    the file on shutdown.
754

755
    """
756
    pass
757

    
758
  def Open(self, force=False):
759
    """Make the device ready for I/O.
760

761
    This is a no-op for the file type.
762

763
    """
764
    pass
765

    
766
  def Close(self):
767
    """Notifies that the device will no longer be used for I/O.
768

769
    This is a no-op for the file type.
770

771
    """
772
    pass
773

    
774
  def Remove(self):
775
    """Remove the file backing the block device.
776

777
    @rtype: boolean
778
    @return: True if the removal was successful
779

780
    """
781
    try:
782
      os.remove(self.dev_path)
783
    except OSError, err:
784
      if err.errno != errno.ENOENT:
785
        base.ThrowError("Can't remove file '%s': %s", self.dev_path, err)
786

    
787
  def Rename(self, new_id):
788
    """Renames the file.
789

790
    """
791
    # TODO: implement rename for file-based storage
792
    base.ThrowError("Rename is not supported for file-based storage")
793

    
794
  def Grow(self, amount, dryrun, backingstore, excl_stor):
795
    """Grow the file
796

797
    @param amount: the amount (in mebibytes) to grow with
798

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

    
817
  def Attach(self):
818
    """Attach to an existing file.
819

820
    Check if this file already exists.
821

822
    @rtype: boolean
823
    @return: True if file exists
824

825
    """
826
    self.attached = os.path.exists(self.dev_path)
827
    return self.attached
828

    
829
  def GetActualSize(self):
830
    """Return the actual disk size.
831

832
    @note: the device needs to be active when this is called
833

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

    
842
  @classmethod
843
  def Create(cls, unique_id, children, size, spindles, params, excl_stor,
844
             dyn_params):
845
    """Create a new file.
846

847
    @param size: the size of file in MiB
848

849
    @rtype: L{bdev.FileStorage}
850
    @return: an instance of FileStorage
851

852
    """
853
    if excl_stor:
854
      raise errors.ProgrammerError("FileStorage device requested with"
855
                                   " exclusive_storage")
856
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
857
      raise ValueError("Invalid configuration data %s" % str(unique_id))
858

    
859
    dev_path = unique_id[1]
860

    
861
    filestorage.CheckFileStoragePathAcceptance(dev_path)
862

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

    
873
    return FileStorage(unique_id, children, size, params, dyn_params)
874

    
875

    
876
class PersistentBlockDevice(base.BlockDev):
877
  """A block device with persistent node
878

879
  May be either directly attached, or exposed through DM (e.g. dm-multipath).
880
  udev helpers are probably required to give persistent, human-friendly
881
  names.
882

883
  For the time being, pathnames are required to lie under /dev.
884

885
  """
886
  def __init__(self, unique_id, children, size, params, dyn_params, *args):
887
    """Attaches to a static block device.
888

889
    The unique_id is a path under /dev.
890

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

    
908
    self.major = self.minor = None
909
    self.Attach()
910

    
911
  @classmethod
912
  def Create(cls, unique_id, children, size, spindles, params, excl_stor,
913
             dyn_params):
914
    """Create a new device
915

916
    This is a noop, we only return a PersistentBlockDevice instance
917

918
    """
919
    if excl_stor:
920
      raise errors.ProgrammerError("Persistent block device requested with"
921
                                   " exclusive_storage")
922
    return PersistentBlockDevice(unique_id, children, 0, params, dyn_params)
923

    
924
  def Remove(self):
925
    """Remove a device
926

927
    This is a noop
928

929
    """
930
    pass
931

    
932
  def Rename(self, new_id):
933
    """Rename this device.
934

935
    """
936
    base.ThrowError("Rename is not supported for PersistentBlockDev storage")
937

    
938
  def Attach(self):
939
    """Attach to an existing block device.
940

941

942
    """
943
    self.attached = False
944
    try:
945
      st = os.stat(self.dev_path)
946
    except OSError, err:
947
      logging.error("Error stat()'ing %s: %s", self.dev_path, str(err))
948
      return False
949

    
950
    if not stat.S_ISBLK(st.st_mode):
951
      logging.error("%s is not a block device", self.dev_path)
952
      return False
953

    
954
    self.major = os.major(st.st_rdev)
955
    self.minor = os.minor(st.st_rdev)
956
    self.attached = True
957

    
958
    return True
959

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

963
    """
964
    pass
965

    
966
  def Shutdown(self):
967
    """Shutdown the device.
968

969
    """
970
    pass
971

    
972
  def Open(self, force=False):
973
    """Make the device ready for I/O.
974

975
    """
976
    pass
977

    
978
  def Close(self):
979
    """Notifies that the device will no longer be used for I/O.
980

981
    """
982
    pass
983

    
984
  def Grow(self, amount, dryrun, backingstore, excl_stor):
985
    """Grow the logical volume.
986

987
    """
988
    base.ThrowError("Grow is not supported for PersistentBlockDev storage")
989

    
990

    
991
class RADOSBlockDevice(base.BlockDev):
992
  """A RADOS Block Device (rbd).
993

994
  This class implements the RADOS Block Device for the backend. You need
995
  the rbd kernel driver, the RADOS Tools and a working RADOS cluster for
996
  this to be functional.
997

998
  """
999
  def __init__(self, unique_id, children, size, params, dyn_params, *args):
1000
    """Attaches to an rbd device.
1001

1002
    """
1003
    super(RADOSBlockDevice, self).__init__(unique_id, children, size, params,
1004
                                           dyn_params, *args)
1005
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1006
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1007

    
1008
    self.driver, self.rbd_name = unique_id
1009
    self.rbd_pool = params[constants.LDP_POOL]
1010

    
1011
    self.major = self.minor = None
1012
    self.Attach()
1013

    
1014
  @classmethod
1015
  def Create(cls, unique_id, children, size, spindles, params, excl_stor,
1016
             dyn_params):
1017
    """Create a new rbd device.
1018

1019
    Provision a new rbd volume inside a RADOS pool.
1020

1021
    """
1022
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1023
      raise errors.ProgrammerError("Invalid configuration data %s" %
1024
                                   str(unique_id))
1025
    if excl_stor:
1026
      raise errors.ProgrammerError("RBD device requested with"
1027
                                   " exclusive_storage")
1028
    rbd_pool = params[constants.LDP_POOL]
1029
    rbd_name = unique_id[1]
1030

    
1031
    # Provision a new rbd volume (Image) inside the RADOS cluster.
1032
    cmd = [constants.RBD_CMD, "create", "-p", rbd_pool,
1033
           rbd_name, "--size", "%s" % size]
1034
    result = utils.RunCmd(cmd)
1035
    if result.failed:
1036
      base.ThrowError("rbd creation failed (%s): %s",
1037
                      result.fail_reason, result.output)
1038

    
1039
    return RADOSBlockDevice(unique_id, children, size, params, dyn_params)
1040

    
1041
  def Remove(self):
1042
    """Remove the rbd device.
1043

1044
    """
1045
    rbd_pool = self.params[constants.LDP_POOL]
1046
    rbd_name = self.unique_id[1]
1047

    
1048
    if not self.minor and not self.Attach():
1049
      # The rbd device doesn't exist.
1050
      return
1051

    
1052
    # First shutdown the device (remove mappings).
1053
    self.Shutdown()
1054

    
1055
    # Remove the actual Volume (Image) from the RADOS cluster.
1056
    cmd = [constants.RBD_CMD, "rm", "-p", rbd_pool, rbd_name]
1057
    result = utils.RunCmd(cmd)
1058
    if result.failed:
1059
      base.ThrowError("Can't remove Volume from cluster with rbd rm: %s - %s",
1060
                      result.fail_reason, result.output)
1061

    
1062
  def Rename(self, new_id):
1063
    """Rename this device.
1064

1065
    """
1066
    pass
1067

    
1068
  def Attach(self):
1069
    """Attach to an existing rbd device.
1070

1071
    This method maps the rbd volume that matches our name with
1072
    an rbd device and then attaches to this device.
1073

1074
    """
1075
    self.attached = False
1076

    
1077
    # Map the rbd volume to a block device under /dev
1078
    self.dev_path = self._MapVolumeToBlockdev(self.unique_id)
1079

    
1080
    try:
1081
      st = os.stat(self.dev_path)
1082
    except OSError, err:
1083
      logging.error("Error stat()'ing %s: %s", self.dev_path, str(err))
1084
      return False
1085

    
1086
    if not stat.S_ISBLK(st.st_mode):
1087
      logging.error("%s is not a block device", self.dev_path)
1088
      return False
1089

    
1090
    self.major = os.major(st.st_rdev)
1091
    self.minor = os.minor(st.st_rdev)
1092
    self.attached = True
1093

    
1094
    return True
1095

    
1096
  def _MapVolumeToBlockdev(self, unique_id):
1097
    """Maps existing rbd volumes to block devices.
1098

1099
    This method should be idempotent if the mapping already exists.
1100

1101
    @rtype: string
1102
    @return: the block device path that corresponds to the volume
1103

1104
    """
1105
    pool = self.params[constants.LDP_POOL]
1106
    name = unique_id[1]
1107

    
1108
    # Check if the mapping already exists.
1109
    rbd_dev = self._VolumeToBlockdev(pool, name)
1110
    if rbd_dev:
1111
      # The mapping exists. Return it.
1112
      return rbd_dev
1113

    
1114
    # The mapping doesn't exist. Create it.
1115
    map_cmd = [constants.RBD_CMD, "map", "-p", pool, name]
1116
    result = utils.RunCmd(map_cmd)
1117
    if result.failed:
1118
      base.ThrowError("rbd map failed (%s): %s",
1119
                      result.fail_reason, result.output)
1120

    
1121
    # Find the corresponding rbd device.
1122
    rbd_dev = self._VolumeToBlockdev(pool, name)
1123
    if not rbd_dev:
1124
      base.ThrowError("rbd map succeeded, but could not find the rbd block"
1125
                      " device in output of showmapped, for volume: %s", name)
1126

    
1127
    # The device was successfully mapped. Return it.
1128
    return rbd_dev
1129

    
1130
  @classmethod
1131
  def _VolumeToBlockdev(cls, pool, volume_name):
1132
    """Do the 'volume name'-to-'rbd block device' resolving.
1133

1134
    @type pool: string
1135
    @param pool: RADOS pool to use
1136
    @type volume_name: string
1137
    @param volume_name: the name of the volume whose device we search for
1138
    @rtype: string or None
1139
    @return: block device path if the volume is mapped, else None
1140

1141
    """
1142
    try:
1143
      # Newer versions of the rbd tool support json output formatting. Use it
1144
      # if available.
1145
      showmap_cmd = [
1146
        constants.RBD_CMD,
1147
        "showmapped",
1148
        "-p",
1149
        pool,
1150
        "--format",
1151
        "json"
1152
        ]
1153
      result = utils.RunCmd(showmap_cmd)
1154
      if result.failed:
1155
        logging.error("rbd JSON output formatting returned error (%s): %s,"
1156
                      "falling back to plain output parsing",
1157
                      result.fail_reason, result.output)
1158
        raise RbdShowmappedJsonError
1159

    
1160
      return cls._ParseRbdShowmappedJson(result.output, volume_name)
1161
    except RbdShowmappedJsonError:
1162
      # For older versions of rbd, we have to parse the plain / text output
1163
      # manually.
1164
      showmap_cmd = [constants.RBD_CMD, "showmapped", "-p", pool]
1165
      result = utils.RunCmd(showmap_cmd)
1166
      if result.failed:
1167
        base.ThrowError("rbd showmapped failed (%s): %s",
1168
                        result.fail_reason, result.output)
1169

    
1170
      return cls._ParseRbdShowmappedPlain(result.output, volume_name)
1171

    
1172
  @staticmethod
1173
  def _ParseRbdShowmappedJson(output, volume_name):
1174
    """Parse the json output of `rbd showmapped'.
1175

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

1179
    @type output: string
1180
    @param output: the json output of `rbd showmapped'
1181
    @type volume_name: string
1182
    @param volume_name: the name of the volume whose device we search for
1183
    @rtype: string or None
1184
    @return: block device path if the volume is mapped, else None
1185

1186
    """
1187
    try:
1188
      devices = serializer.LoadJson(output)
1189
    except ValueError, err:
1190
      base.ThrowError("Unable to parse JSON data: %s" % err)
1191

    
1192
    rbd_dev = None
1193
    for d in devices.values(): # pylint: disable=E1103
1194
      try:
1195
        name = d["name"]
1196
      except KeyError:
1197
        base.ThrowError("'name' key missing from json object %s", devices)
1198

    
1199
      if name == volume_name:
1200
        if rbd_dev is not None:
1201
          base.ThrowError("rbd volume %s is mapped more than once", volume_name)
1202

    
1203
        rbd_dev = d["device"]
1204

    
1205
    return rbd_dev
1206

    
1207
  @staticmethod
1208
  def _ParseRbdShowmappedPlain(output, volume_name):
1209
    """Parse the (plain / text) output of `rbd showmapped'.
1210

1211
    This method parses the output of `rbd showmapped' and returns
1212
    the rbd block device path (e.g. /dev/rbd0) that matches the
1213
    given rbd volume.
1214

1215
    @type output: string
1216
    @param output: the plain text output of `rbd showmapped'
1217
    @type volume_name: string
1218
    @param volume_name: the name of the volume whose device we search for
1219
    @rtype: string or None
1220
    @return: block device path if the volume is mapped, else None
1221

1222
    """
1223
    allfields = 5
1224
    volumefield = 2
1225
    devicefield = 4
1226

    
1227
    lines = output.splitlines()
1228

    
1229
    # Try parsing the new output format (ceph >= 0.55).
1230
    splitted_lines = map(lambda l: l.split(), lines)
1231

    
1232
    # Check for empty output.
1233
    if not splitted_lines:
1234
      return None
1235

    
1236
    # Check showmapped output, to determine number of fields.
1237
    field_cnt = len(splitted_lines[0])
1238
    if field_cnt != allfields:
1239
      # Parsing the new format failed. Fallback to parsing the old output
1240
      # format (< 0.55).
1241
      splitted_lines = map(lambda l: l.split("\t"), lines)
1242
      if field_cnt != allfields:
1243
        base.ThrowError("Cannot parse rbd showmapped output expected %s fields,"
1244
                        " found %s", allfields, field_cnt)
1245

    
1246
    matched_lines = \
1247
      filter(lambda l: len(l) == allfields and l[volumefield] == volume_name,
1248
             splitted_lines)
1249

    
1250
    if len(matched_lines) > 1:
1251
      base.ThrowError("rbd volume %s mapped more than once", volume_name)
1252

    
1253
    if matched_lines:
1254
      # rbd block device found. Return it.
1255
      rbd_dev = matched_lines[0][devicefield]
1256
      return rbd_dev
1257

    
1258
    # The given volume is not mapped.
1259
    return None
1260

    
1261
  def Assemble(self):
1262
    """Assemble the device.
1263

1264
    """
1265
    pass
1266

    
1267
  def Shutdown(self):
1268
    """Shutdown the device.
1269

1270
    """
1271
    if not self.minor and not self.Attach():
1272
      # The rbd device doesn't exist.
1273
      return
1274

    
1275
    # Unmap the block device from the Volume.
1276
    self._UnmapVolumeFromBlockdev(self.unique_id)
1277

    
1278
    self.minor = None
1279
    self.dev_path = None
1280

    
1281
  def _UnmapVolumeFromBlockdev(self, unique_id):
1282
    """Unmaps the rbd device from the Volume it is mapped.
1283

1284
    Unmaps the rbd device from the Volume it was previously mapped to.
1285
    This method should be idempotent if the Volume isn't mapped.
1286

1287
    """
1288
    pool = self.params[constants.LDP_POOL]
1289
    name = unique_id[1]
1290

    
1291
    # Check if the mapping already exists.
1292
    rbd_dev = self._VolumeToBlockdev(pool, name)
1293

    
1294
    if rbd_dev:
1295
      # The mapping exists. Unmap the rbd device.
1296
      unmap_cmd = [constants.RBD_CMD, "unmap", "%s" % rbd_dev]
1297
      result = utils.RunCmd(unmap_cmd)
1298
      if result.failed:
1299
        base.ThrowError("rbd unmap failed (%s): %s",
1300
                        result.fail_reason, result.output)
1301

    
1302
  def Open(self, force=False):
1303
    """Make the device ready for I/O.
1304

1305
    """
1306
    pass
1307

    
1308
  def Close(self):
1309
    """Notifies that the device will no longer be used for I/O.
1310

1311
    """
1312
    pass
1313

    
1314
  def Grow(self, amount, dryrun, backingstore, excl_stor):
1315
    """Grow the Volume.
1316

1317
    @type amount: integer
1318
    @param amount: the amount (in mebibytes) to grow with
1319
    @type dryrun: boolean
1320
    @param dryrun: whether to execute the operation in simulation mode
1321
        only, without actually increasing the size
1322

1323
    """
1324
    if not backingstore:
1325
      return
1326
    if not self.Attach():
1327
      base.ThrowError("Can't attach to rbd device during Grow()")
1328

    
1329
    if dryrun:
1330
      # the rbd tool does not support dry runs of resize operations.
1331
      # Since rbd volumes are thinly provisioned, we assume
1332
      # there is always enough free space for the operation.
1333
      return
1334

    
1335
    rbd_pool = self.params[constants.LDP_POOL]
1336
    rbd_name = self.unique_id[1]
1337
    new_size = self.size + amount
1338

    
1339
    # Resize the rbd volume (Image) inside the RADOS cluster.
1340
    cmd = [constants.RBD_CMD, "resize", "-p", rbd_pool,
1341
           rbd_name, "--size", "%s" % new_size]
1342
    result = utils.RunCmd(cmd)
1343
    if result.failed:
1344
      base.ThrowError("rbd resize failed (%s): %s",
1345
                      result.fail_reason, result.output)
1346

    
1347
  def GetUserspaceAccessUri(self, hypervisor):
1348
    """Generate KVM userspace URIs to be used as `-drive file` settings.
1349

1350
    @see: L{BlockDev.GetUserspaceAccessUri}
1351

1352
    """
1353
    if hypervisor == constants.HT_KVM:
1354
      return "rbd:" + self.rbd_pool + "/" + self.rbd_name
1355
    else:
1356
      base.ThrowError("Hypervisor %s doesn't support RBD userspace access" %
1357
                      hypervisor)
1358

    
1359

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

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

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

1370
    """
1371
    super(ExtStorageDevice, self).__init__(unique_id, children, size, params,
1372
                                           dyn_params)
1373
    (self.name, self.uuid) = args
1374

    
1375
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1376
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1377

    
1378
    self.driver, self.vol_name = unique_id
1379
    self.ext_params = params
1380

    
1381
    self.major = self.minor = None
1382
    self.Attach()
1383

    
1384
  @classmethod
1385
  def Create(cls, unique_id, children, size, spindles, params, excl_stor,
1386
             dyn_params, *args):
1387
    """Create a new extstorage device.
1388

1389
    Provision a new volume using an extstorage provider, which will
1390
    then be mapped to a block device.
1391

1392
    """
1393
    (name, uuid) = args
1394

    
1395
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1396
      raise errors.ProgrammerError("Invalid configuration data %s" %
1397
                                   str(unique_id))
1398
    if excl_stor:
1399
      raise errors.ProgrammerError("extstorage device requested with"
1400
                                   " exclusive_storage")
1401

    
1402
    # Call the External Storage's create script,
1403
    # to provision a new Volume inside the External Storage
1404
    _ExtStorageAction(constants.ES_ACTION_CREATE, unique_id,
1405
                      params, size=str(size), name=name, uuid=uuid)
1406

    
1407
    return ExtStorageDevice(unique_id, children, size, params, dyn_params)
1408

    
1409
  def Remove(self):
1410
    """Remove the extstorage device.
1411

1412
    """
1413
    if not self.minor and not self.Attach():
1414
      # The extstorage device doesn't exist.
1415
      return
1416

    
1417
    # First shutdown the device (remove mappings).
1418
    self.Shutdown()
1419

    
1420
    # Call the External Storage's remove script,
1421
    # to remove the Volume from the External Storage
1422
    _ExtStorageAction(constants.ES_ACTION_REMOVE, self.unique_id,
1423
                      self.ext_params, name=self.name, uuid=self.uuid)
1424

    
1425
  def Rename(self, new_id):
1426
    """Rename this device.
1427

1428
    """
1429
    pass
1430

    
1431
  def Attach(self):
1432
    """Attach to an existing extstorage device.
1433

1434
    This method maps the extstorage volume that matches our name with
1435
    a corresponding block device and then attaches to this device.
1436

1437
    """
1438
    self.attached = False
1439

    
1440
    # Call the External Storage's attach script,
1441
    # to attach an existing Volume to a block device under /dev
1442
    self.dev_path = _ExtStorageAction(constants.ES_ACTION_ATTACH,
1443
                                      self.unique_id, self.ext_params,
1444
                                      name=self.name, uuid=self.uuid)
1445

    
1446
    try:
1447
      st = os.stat(self.dev_path)
1448
    except OSError, err:
1449
      logging.error("Error stat()'ing %s: %s", self.dev_path, str(err))
1450
      return False
1451

    
1452
    if not stat.S_ISBLK(st.st_mode):
1453
      logging.error("%s is not a block device", self.dev_path)
1454
      return False
1455

    
1456
    self.major = os.major(st.st_rdev)
1457
    self.minor = os.minor(st.st_rdev)
1458
    self.attached = True
1459

    
1460
    return True
1461

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

1465
    """
1466
    pass
1467

    
1468
  def Shutdown(self):
1469
    """Shutdown the device.
1470

1471
    """
1472
    if not self.minor and not self.Attach():
1473
      # The extstorage device doesn't exist.
1474
      return
1475

    
1476
    # Call the External Storage's detach script,
1477
    # to detach an existing Volume from it's block device under /dev
1478
    _ExtStorageAction(constants.ES_ACTION_DETACH, self.unique_id,
1479
                      self.ext_params, name=self.name, uuid=self.uuid)
1480

    
1481
    self.minor = None
1482
    self.dev_path = None
1483

    
1484
  def Open(self, force=False):
1485
    """Make the device ready for I/O.
1486

1487
    """
1488
    pass
1489

    
1490
  def Close(self):
1491
    """Notifies that the device will no longer be used for I/O.
1492

1493
    """
1494
    pass
1495

    
1496
  def Grow(self, amount, dryrun, backingstore, excl_stor):
1497
    """Grow the Volume.
1498

1499
    @type amount: integer
1500
    @param amount: the amount (in mebibytes) to grow with
1501
    @type dryrun: boolean
1502
    @param dryrun: whether to execute the operation in simulation mode
1503
        only, without actually increasing the size
1504

1505
    """
1506
    if not backingstore:
1507
      return
1508
    if not self.Attach():
1509
      base.ThrowError("Can't attach to extstorage device during Grow()")
1510

    
1511
    if dryrun:
1512
      # we do not support dry runs of resize operations for now.
1513
      return
1514

    
1515
    new_size = self.size + amount
1516

    
1517
    # Call the External Storage's grow script,
1518
    # to grow an existing Volume inside the External Storage
1519
    _ExtStorageAction(constants.ES_ACTION_GROW, self.unique_id,
1520
                      self.ext_params, size=str(self.size), grow=str(new_size),
1521
                      name=self.name, uuid=self.uuid)
1522

    
1523
  def SetInfo(self, text):
1524
    """Update metadata with info text.
1525

1526
    """
1527
    # Replace invalid characters
1528
    text = re.sub("^[^A-Za-z0-9_+.]", "_", text)
1529
    text = re.sub("[^-A-Za-z0-9_+.]", "_", text)
1530

    
1531
    # Only up to 128 characters are allowed
1532
    text = text[:128]
1533

    
1534
    # Call the External Storage's setinfo script,
1535
    # to set metadata for an existing Volume inside the External Storage
1536
    _ExtStorageAction(constants.ES_ACTION_SETINFO, self.unique_id,
1537
                      self.ext_params, metadata=text,
1538
                      name=self.name, uuid=self.uuid)
1539

    
1540

    
1541
def _ExtStorageAction(action, unique_id, ext_params,
1542
                      size=None, grow=None, metadata=None,
1543
                      name=None, uuid=None):
1544
  """Take an External Storage action.
1545

1546
  Take an External Storage action concerning or affecting
1547
  a specific Volume inside the External Storage.
1548

1549
  @type action: string
1550
  @param action: which action to perform. One of:
1551
                 create / remove / grow / attach / detach
1552
  @type unique_id: tuple (driver, vol_name)
1553
  @param unique_id: a tuple containing the type of ExtStorage (driver)
1554
                    and the Volume name
1555
  @type ext_params: dict
1556
  @param ext_params: ExtStorage parameters
1557
  @type size: integer
1558
  @param size: the size of the Volume in mebibytes
1559
  @type grow: integer
1560
  @param grow: the new size in mebibytes (after grow)
1561
  @type metadata: string
1562
  @param metadata: metadata info of the Volume, for use by the provider
1563
  @type name: string
1564
  @param name: name of the Volume (objects.Disk.name)
1565
  @type uuid: string
1566
  @param uuid: uuid of the Volume (objects.Disk.uuid)
1567
  @rtype: None or a block device path (during attach)
1568

1569
  """
1570
  driver, vol_name = unique_id
1571

    
1572
  # Create an External Storage instance of type `driver'
1573
  status, inst_es = ExtStorageFromDisk(driver)
1574
  if not status:
1575
    base.ThrowError("%s" % inst_es)
1576

    
1577
  # Create the basic environment for the driver's scripts
1578
  create_env = _ExtStorageEnvironment(unique_id, ext_params, size,
1579
                                      grow, metadata, name, uuid)
1580

    
1581
  # Do not use log file for action `attach' as we need
1582
  # to get the output from RunResult
1583
  # TODO: find a way to have a log file for attach too
1584
  logfile = None
1585
  if action is not constants.ES_ACTION_ATTACH:
1586
    logfile = _VolumeLogName(action, driver, vol_name)
1587

    
1588
  # Make sure the given action results in a valid script
1589
  if action not in constants.ES_SCRIPTS:
1590
    base.ThrowError("Action '%s' doesn't result in a valid ExtStorage script" %
1591
                    action)
1592

    
1593
  # Find out which external script to run according the given action
1594
  script_name = action + "_script"
1595
  script = getattr(inst_es, script_name)
1596

    
1597
  # Run the external script
1598
  result = utils.RunCmd([script], env=create_env,
1599
                        cwd=inst_es.path, output=logfile,)
1600
  if result.failed:
1601
    logging.error("External storage's %s command '%s' returned"
1602
                  " error: %s, logfile: %s, output: %s",
1603
                  action, result.cmd, result.fail_reason,
1604
                  logfile, result.output)
1605

    
1606
    # If logfile is 'None' (during attach), it breaks TailFile
1607
    # TODO: have a log file for attach too
1608
    if action is not constants.ES_ACTION_ATTACH:
1609
      lines = [utils.SafeEncode(val)
1610
               for val in utils.TailFile(logfile, lines=20)]
1611
    else:
1612
      lines = result.output[-20:]
1613

    
1614
    base.ThrowError("External storage's %s script failed (%s), last"
1615
                    " lines of output:\n%s",
1616
                    action, result.fail_reason, "\n".join(lines))
1617

    
1618
  if action == constants.ES_ACTION_ATTACH:
1619
    return result.stdout
1620

    
1621

    
1622
def ExtStorageFromDisk(name, base_dir=None):
1623
  """Create an ExtStorage instance from disk.
1624

1625
  This function will return an ExtStorage instance
1626
  if the given name is a valid ExtStorage name.
1627

1628
  @type base_dir: string
1629
  @keyword base_dir: Base directory containing ExtStorage installations.
1630
                     Defaults to a search in all the ES_SEARCH_PATH dirs.
1631
  @rtype: tuple
1632
  @return: True and the ExtStorage instance if we find a valid one, or
1633
      False and the diagnose message on error
1634

1635
  """
1636
  if base_dir is None:
1637
    es_base_dir = pathutils.ES_SEARCH_PATH
1638
  else:
1639
    es_base_dir = [base_dir]
1640

    
1641
  es_dir = utils.FindFile(name, es_base_dir, os.path.isdir)
1642

    
1643
  if es_dir is None:
1644
    return False, ("Directory for External Storage Provider %s not"
1645
                   " found in search path" % name)
1646

    
1647
  # ES Files dictionary, we will populate it with the absolute path
1648
  # names; if the value is True, then it is a required file, otherwise
1649
  # an optional one
1650
  es_files = dict.fromkeys(constants.ES_SCRIPTS, True)
1651

    
1652
  es_files[constants.ES_PARAMETERS_FILE] = True
1653

    
1654
  for (filename, _) in es_files.items():
1655
    es_files[filename] = utils.PathJoin(es_dir, filename)
1656

    
1657
    try:
1658
      st = os.stat(es_files[filename])
1659
    except EnvironmentError, err:
1660
      return False, ("File '%s' under path '%s' is missing (%s)" %
1661
                     (filename, es_dir, utils.ErrnoOrStr(err)))
1662

    
1663
    if not stat.S_ISREG(stat.S_IFMT(st.st_mode)):
1664
      return False, ("File '%s' under path '%s' is not a regular file" %
1665
                     (filename, es_dir))
1666

    
1667
    if filename in constants.ES_SCRIPTS:
1668
      if stat.S_IMODE(st.st_mode) & stat.S_IXUSR != stat.S_IXUSR:
1669
        return False, ("File '%s' under path '%s' is not executable" %
1670
                       (filename, es_dir))
1671

    
1672
  parameters = []
1673
  if constants.ES_PARAMETERS_FILE in es_files:
1674
    parameters_file = es_files[constants.ES_PARAMETERS_FILE]
1675
    try:
1676
      parameters = utils.ReadFile(parameters_file).splitlines()
1677
    except EnvironmentError, err:
1678
      return False, ("Error while reading the EXT parameters file at %s: %s" %
1679
                     (parameters_file, utils.ErrnoOrStr(err)))
1680
    parameters = [v.split(None, 1) for v in parameters]
1681

    
1682
  es_obj = \
1683
    objects.ExtStorage(name=name, path=es_dir,
1684
                       create_script=es_files[constants.ES_SCRIPT_CREATE],
1685
                       remove_script=es_files[constants.ES_SCRIPT_REMOVE],
1686
                       grow_script=es_files[constants.ES_SCRIPT_GROW],
1687
                       attach_script=es_files[constants.ES_SCRIPT_ATTACH],
1688
                       detach_script=es_files[constants.ES_SCRIPT_DETACH],
1689
                       setinfo_script=es_files[constants.ES_SCRIPT_SETINFO],
1690
                       verify_script=es_files[constants.ES_SCRIPT_VERIFY],
1691
                       supported_parameters=parameters)
1692
  return True, es_obj
1693

    
1694

    
1695
def _ExtStorageEnvironment(unique_id, ext_params,
1696
                           size=None, grow=None, metadata=None,
1697
                           name=None, uuid=None):
1698
  """Calculate the environment for an External Storage script.
1699

1700
  @type unique_id: tuple (driver, vol_name)
1701
  @param unique_id: ExtStorage pool and name of the Volume
1702
  @type ext_params: dict
1703
  @param ext_params: the EXT parameters
1704
  @type size: string
1705
  @param size: size of the Volume (in mebibytes)
1706
  @type grow: string
1707
  @param grow: new size of Volume after grow (in mebibytes)
1708
  @type metadata: string
1709
  @param metadata: metadata info of the Volume
1710
  @type name: string
1711
  @param name: name of the Volume (objects.Disk.name)
1712
  @type uuid: string
1713
  @param uuid: uuid of the Volume (objects.Disk.uuid)
1714
  @rtype: dict
1715
  @return: dict of environment variables
1716

1717
  """
1718
  vol_name = unique_id[1]
1719

    
1720
  result = {}
1721
  result["VOL_NAME"] = vol_name
1722

    
1723
  # EXT params
1724
  for pname, pvalue in ext_params.items():
1725
    result["EXTP_%s" % pname.upper()] = str(pvalue)
1726

    
1727
  if size is not None:
1728
    result["VOL_SIZE"] = size
1729

    
1730
  if grow is not None:
1731
    result["VOL_NEW_SIZE"] = grow
1732

    
1733
  if metadata is not None:
1734
    result["VOL_METADATA"] = metadata
1735

    
1736
  if name is not None:
1737
    result["VOL_CNAME"] = name
1738

    
1739
  if uuid is not None:
1740
    result["VOL_UUID"] = uuid
1741

    
1742
  return result
1743

    
1744

    
1745
def _VolumeLogName(kind, es_name, volume):
1746
  """Compute the ExtStorage log filename for a given Volume and operation.
1747

1748
  @type kind: string
1749
  @param kind: the operation type (e.g. create, remove etc.)
1750
  @type es_name: string
1751
  @param es_name: the ExtStorage name
1752
  @type volume: string
1753
  @param volume: the name of the Volume inside the External Storage
1754

1755
  """
1756
  # Check if the extstorage log dir is a valid dir
1757
  if not os.path.isdir(pathutils.LOG_ES_DIR):
1758
    base.ThrowError("Cannot find log directory: %s", pathutils.LOG_ES_DIR)
1759

    
1760
  # TODO: Use tempfile.mkstemp to create unique filename
1761
  basename = ("%s-%s-%s-%s.log" %
1762
              (kind, es_name, volume, utils.TimestampForFilename()))
1763
  return utils.PathJoin(pathutils.LOG_ES_DIR, basename)
1764

    
1765

    
1766
DEV_MAP = {
1767
  constants.DT_PLAIN: LogicalVolume,
1768
  constants.DT_DRBD8: drbd.DRBD8Dev,
1769
  constants.DT_BLOCK: PersistentBlockDevice,
1770
  constants.DT_RBD: RADOSBlockDevice,
1771
  constants.DT_EXT: ExtStorageDevice,
1772
  constants.DT_FILE: FileStorage,
1773
  constants.DT_SHARED_FILE: FileStorage,
1774
  }
1775

    
1776

    
1777
def _VerifyDiskType(dev_type):
1778
  if dev_type not in DEV_MAP:
1779
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1780

    
1781

    
1782
def _VerifyDiskParams(disk):
1783
  """Verifies if all disk parameters are set.
1784

1785
  """
1786
  missing = set(constants.DISK_LD_DEFAULTS[disk.dev_type]) - set(disk.params)
1787
  if missing:
1788
    raise errors.ProgrammerError("Block device is missing disk parameters: %s" %
1789
                                 missing)
1790

    
1791

    
1792
def FindDevice(disk, children):
1793
  """Search for an existing, assembled device.
1794

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

1798
  @type disk: L{objects.Disk}
1799
  @param disk: the disk object to find
1800
  @type children: list of L{bdev.BlockDev}
1801
  @param children: the list of block devices that are children of the device
1802
                  represented by the disk parameter
1803

1804
  """
1805
  _VerifyDiskType(disk.dev_type)
1806
  device = DEV_MAP[disk.dev_type](disk.logical_id, children, disk.size,
1807
                                  disk.params, disk.dynamic_params,
1808
                                  disk.name, disk.uuid)
1809
  if not device.attached:
1810
    return None
1811
  return device
1812

    
1813

    
1814
def Assemble(disk, children):
1815
  """Try to attach or assemble an existing device.
1816

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

1820
  @type disk: L{objects.Disk}
1821
  @param disk: the disk object to assemble
1822
  @type children: list of L{bdev.BlockDev}
1823
  @param children: the list of block devices that are children of the device
1824
                  represented by the disk parameter
1825

1826
  """
1827
  _VerifyDiskType(disk.dev_type)
1828
  _VerifyDiskParams(disk)
1829
  device = DEV_MAP[disk.dev_type](disk.logical_id, children, disk.size,
1830
                                  disk.params, disk.dynamic_params,
1831
                                  disk.name, disk.uuid)
1832
  device.Assemble()
1833
  return device
1834

    
1835

    
1836
def Create(disk, children, excl_stor):
1837
  """Create a device.
1838

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

1849
  """
1850
  _VerifyDiskType(disk.dev_type)
1851
  _VerifyDiskParams(disk)
1852
  device = DEV_MAP[disk.dev_type].Create(disk.logical_id, children, disk.size,
1853
                                         disk.spindles, disk.params, excl_stor,
1854
                                         disk.dynamic_params,
1855
                                         disk.name, disk.uuid)
1856
  return device