Statistics
| Branch: | Tag: | Revision:

root / lib / storage / bdev.py @ 8106dd64

History | View | Annotate | Download (53.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
"""
25

    
26
import re
27
import stat
28
import os
29
import logging
30
import math
31

    
32
from ganeti import utils
33
from ganeti import errors
34
from ganeti import constants
35
from ganeti import objects
36
from ganeti import compat
37
from ganeti import pathutils
38
from ganeti import serializer
39
from ganeti.storage import base
40
from ganeti.storage import drbd
41
from ganeti.storage.filestorage import FileStorage
42
from ganeti.storage.gluster import GlusterStorage
43

    
44

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

48
  """
49
  pass
50

    
51

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

55
  @param result: result from RunCmd
56

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

    
62

    
63
class LogicalVolume(base.BlockDev):
64
  """Logical Volume block device.
65

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

    
72
  def __init__(self, unique_id, children, size, params, dyn_params):
73
    """Attaches to a LV device.
74

75
    The unique_id is a tuple (vg_name, lv_name)
76

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

222
    """
223
    if not fields:
224
      raise errors.ProgrammerError("No fields specified")
225

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

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

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

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

    
243
      data.append(splitted_fields)
244

    
245
    return data
246

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

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

255
    @rtype: list
256
    @return: list of objects.LvmPvInfo objects
257

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

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

    
302
    return data
303

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

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

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

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

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

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

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

345
    """
346
    (_, free_pvs, num_pvs) = cls._GetRawFreePvInfo(vg_name)
347
    return (free_pvs, num_pvs)
348

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

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

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

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

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

    
384
    return data
385

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

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

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

    
400
  def Remove(self):
401
    """Remove this logical volume.
402

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

    
413
  def Rename(self, new_id):
414
    """Rename this logical volume.
415

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

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

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

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

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

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

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

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

    
467
    return (status, major, minor, pe_size, stripes, pv_names)
468

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

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

    
498
  def Attach(self):
499
    """Attach to an existing LV.
500

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

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

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

    
523
  def Assemble(self):
524
    """Assemble the device.
525

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

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

    
535
  def Shutdown(self):
536
    """Shutdown the device.
537

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

541
    """
542
    pass
543

    
544
  def GetSyncStatus(self):
545
    """Returns the sync status of the device.
546

547
    If this device is a mirroring device, this function returns the
548
    status of the mirror.
549

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

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

561
    The status was already read in Attach, so we just return it.
562

563
    @rtype: objects.BlockDevStatus
564

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

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

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

582
    This is a no-op for the LV device type.
583

584
    """
585
    pass
586

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

590
    This is a no-op for the LV device type.
591

592
    """
593
    pass
594

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

598
    @returns: tuple (vg, lv)
599

600
    """
601
    snap_name = self._lv_name + ".snap"
602

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

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

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

    
619
    return (self._vg_name, snap_name)
620

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

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

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

    
635
  def SetInfo(self, text):
636
    """Update metadata with info text.
637

638
    """
639
    base.BlockDev.SetInfo(self, text)
640

    
641
    self._RemoveOldInfo()
642

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

    
647
    # Only up to 128 characters are allowed
648
    text = text[:128]
649

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

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

655
    @rtype: float
656
    @return: available space in Mib
657

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

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

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

    
709
  def GetActualSpindles(self):
710
    """Return the number of spindles used.
711

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

    
716

    
717
class PersistentBlockDevice(base.BlockDev):
718
  """A block device with persistent node
719

720
  May be either directly attached, or exposed through DM (e.g. dm-multipath).
721
  udev helpers are probably required to give persistent, human-friendly
722
  names.
723

724
  For the time being, pathnames are required to lie under /dev.
725

726
  """
727
  def __init__(self, unique_id, children, size, params, dyn_params):
728
    """Attaches to a static block device.
729

730
    The unique_id is a path under /dev.
731

732
    """
733
    super(PersistentBlockDevice, self).__init__(unique_id, children, size,
734
                                                params, dyn_params)
735
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
736
      raise ValueError("Invalid configuration data %s" % str(unique_id))
737
    self.dev_path = unique_id[1]
738
    if not os.path.realpath(self.dev_path).startswith("/dev/"):
739
      raise ValueError("Full path '%s' lies outside /dev" %
740
                              os.path.realpath(self.dev_path))
741
    # TODO: this is just a safety guard checking that we only deal with devices
742
    # we know how to handle. In the future this will be integrated with
743
    # external storage backends and possible values will probably be collected
744
    # from the cluster configuration.
745
    if unique_id[0] != constants.BLOCKDEV_DRIVER_MANUAL:
746
      raise ValueError("Got persistent block device of invalid type: %s" %
747
                       unique_id[0])
748

    
749
    self.major = self.minor = None
750
    self.Attach()
751

    
752
  @classmethod
753
  def Create(cls, unique_id, children, size, spindles, params, excl_stor,
754
             dyn_params):
755
    """Create a new device
756

757
    This is a noop, we only return a PersistentBlockDevice instance
758

759
    """
760
    if excl_stor:
761
      raise errors.ProgrammerError("Persistent block device requested with"
762
                                   " exclusive_storage")
763
    return PersistentBlockDevice(unique_id, children, 0, params, dyn_params)
764

    
765
  def Remove(self):
766
    """Remove a device
767

768
    This is a noop
769

770
    """
771
    pass
772

    
773
  def Rename(self, new_id):
774
    """Rename this device.
775

776
    """
777
    base.ThrowError("Rename is not supported for PersistentBlockDev storage")
778

    
779
  def Attach(self):
780
    """Attach to an existing block device.
781

782

783
    """
784
    self.attached = False
785
    try:
786
      st = os.stat(self.dev_path)
787
    except OSError, err:
788
      logging.error("Error stat()'ing %s: %s", self.dev_path, str(err))
789
      return False
790

    
791
    if not stat.S_ISBLK(st.st_mode):
792
      logging.error("%s is not a block device", self.dev_path)
793
      return False
794

    
795
    self.major = os.major(st.st_rdev)
796
    self.minor = os.minor(st.st_rdev)
797
    self.attached = True
798

    
799
    return True
800

    
801
  def Assemble(self):
802
    """Assemble the device.
803

804
    """
805
    pass
806

    
807
  def Shutdown(self):
808
    """Shutdown the device.
809

810
    """
811
    pass
812

    
813
  def Open(self, force=False):
814
    """Make the device ready for I/O.
815

816
    """
817
    pass
818

    
819
  def Close(self):
820
    """Notifies that the device will no longer be used for I/O.
821

822
    """
823
    pass
824

    
825
  def Grow(self, amount, dryrun, backingstore, excl_stor):
826
    """Grow the logical volume.
827

828
    """
829
    base.ThrowError("Grow is not supported for PersistentBlockDev storage")
830

    
831

    
832
class RADOSBlockDevice(base.BlockDev):
833
  """A RADOS Block Device (rbd).
834

835
  This class implements the RADOS Block Device for the backend. You need
836
  the rbd kernel driver, the RADOS Tools and a working RADOS cluster for
837
  this to be functional.
838

839
  """
840
  def __init__(self, unique_id, children, size, params, dyn_params):
841
    """Attaches to an rbd device.
842

843
    """
844
    super(RADOSBlockDevice, self).__init__(unique_id, children, size, params,
845
                                           dyn_params)
846
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
847
      raise ValueError("Invalid configuration data %s" % str(unique_id))
848

    
849
    self.driver, self.rbd_name = unique_id
850
    self.rbd_pool = params[constants.LDP_POOL]
851

    
852
    self.major = self.minor = None
853
    self.Attach()
854

    
855
  @classmethod
856
  def Create(cls, unique_id, children, size, spindles, params, excl_stor,
857
             dyn_params):
858
    """Create a new rbd device.
859

860
    Provision a new rbd volume inside a RADOS pool.
861

862
    """
863
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
864
      raise errors.ProgrammerError("Invalid configuration data %s" %
865
                                   str(unique_id))
866
    if excl_stor:
867
      raise errors.ProgrammerError("RBD device requested with"
868
                                   " exclusive_storage")
869
    rbd_pool = params[constants.LDP_POOL]
870
    rbd_name = unique_id[1]
871

    
872
    # Provision a new rbd volume (Image) inside the RADOS cluster.
873
    cmd = [constants.RBD_CMD, "create", "-p", rbd_pool,
874
           rbd_name, "--size", "%s" % size]
875
    result = utils.RunCmd(cmd)
876
    if result.failed:
877
      base.ThrowError("rbd creation failed (%s): %s",
878
                      result.fail_reason, result.output)
879

    
880
    return RADOSBlockDevice(unique_id, children, size, params, dyn_params)
881

    
882
  def Remove(self):
883
    """Remove the rbd device.
884

885
    """
886
    rbd_pool = self.params[constants.LDP_POOL]
887
    rbd_name = self.unique_id[1]
888

    
889
    if not self.minor and not self.Attach():
890
      # The rbd device doesn't exist.
891
      return
892

    
893
    # First shutdown the device (remove mappings).
894
    self.Shutdown()
895

    
896
    # Remove the actual Volume (Image) from the RADOS cluster.
897
    cmd = [constants.RBD_CMD, "rm", "-p", rbd_pool, rbd_name]
898
    result = utils.RunCmd(cmd)
899
    if result.failed:
900
      base.ThrowError("Can't remove Volume from cluster with rbd rm: %s - %s",
901
                      result.fail_reason, result.output)
902

    
903
  def Rename(self, new_id):
904
    """Rename this device.
905

906
    """
907
    pass
908

    
909
  def Attach(self):
910
    """Attach to an existing rbd device.
911

912
    This method maps the rbd volume that matches our name with
913
    an rbd device and then attaches to this device.
914

915
    """
916
    self.attached = False
917

    
918
    # Map the rbd volume to a block device under /dev
919
    self.dev_path = self._MapVolumeToBlockdev(self.unique_id)
920

    
921
    try:
922
      st = os.stat(self.dev_path)
923
    except OSError, err:
924
      logging.error("Error stat()'ing %s: %s", self.dev_path, str(err))
925
      return False
926

    
927
    if not stat.S_ISBLK(st.st_mode):
928
      logging.error("%s is not a block device", self.dev_path)
929
      return False
930

    
931
    self.major = os.major(st.st_rdev)
932
    self.minor = os.minor(st.st_rdev)
933
    self.attached = True
934

    
935
    return True
936

    
937
  def _MapVolumeToBlockdev(self, unique_id):
938
    """Maps existing rbd volumes to block devices.
939

940
    This method should be idempotent if the mapping already exists.
941

942
    @rtype: string
943
    @return: the block device path that corresponds to the volume
944

945
    """
946
    pool = self.params[constants.LDP_POOL]
947
    name = unique_id[1]
948

    
949
    # Check if the mapping already exists.
950
    rbd_dev = self._VolumeToBlockdev(pool, name)
951
    if rbd_dev:
952
      # The mapping exists. Return it.
953
      return rbd_dev
954

    
955
    # The mapping doesn't exist. Create it.
956
    map_cmd = [constants.RBD_CMD, "map", "-p", pool, name]
957
    result = utils.RunCmd(map_cmd)
958
    if result.failed:
959
      base.ThrowError("rbd map failed (%s): %s",
960
                      result.fail_reason, result.output)
961

    
962
    # Find the corresponding rbd device.
963
    rbd_dev = self._VolumeToBlockdev(pool, name)
964
    if not rbd_dev:
965
      base.ThrowError("rbd map succeeded, but could not find the rbd block"
966
                      " device in output of showmapped, for volume: %s", name)
967

    
968
    # The device was successfully mapped. Return it.
969
    return rbd_dev
970

    
971
  @classmethod
972
  def _VolumeToBlockdev(cls, pool, volume_name):
973
    """Do the 'volume name'-to-'rbd block device' resolving.
974

975
    @type pool: string
976
    @param pool: RADOS pool to use
977
    @type volume_name: string
978
    @param volume_name: the name of the volume whose device we search for
979
    @rtype: string or None
980
    @return: block device path if the volume is mapped, else None
981

982
    """
983
    try:
984
      # Newer versions of the rbd tool support json output formatting. Use it
985
      # if available.
986
      showmap_cmd = [
987
        constants.RBD_CMD,
988
        "showmapped",
989
        "-p",
990
        pool,
991
        "--format",
992
        "json"
993
        ]
994
      result = utils.RunCmd(showmap_cmd)
995
      if result.failed:
996
        logging.error("rbd JSON output formatting returned error (%s): %s,"
997
                      "falling back to plain output parsing",
998
                      result.fail_reason, result.output)
999
        raise RbdShowmappedJsonError
1000

    
1001
      return cls._ParseRbdShowmappedJson(result.output, volume_name)
1002
    except RbdShowmappedJsonError:
1003
      # For older versions of rbd, we have to parse the plain / text output
1004
      # manually.
1005
      showmap_cmd = [constants.RBD_CMD, "showmapped", "-p", pool]
1006
      result = utils.RunCmd(showmap_cmd)
1007
      if result.failed:
1008
        base.ThrowError("rbd showmapped failed (%s): %s",
1009
                        result.fail_reason, result.output)
1010

    
1011
      return cls._ParseRbdShowmappedPlain(result.output, volume_name)
1012

    
1013
  @staticmethod
1014
  def _ParseRbdShowmappedJson(output, volume_name):
1015
    """Parse the json output of `rbd showmapped'.
1016

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

1020
    @type output: string
1021
    @param output: the json output of `rbd showmapped'
1022
    @type volume_name: string
1023
    @param volume_name: the name of the volume whose device we search for
1024
    @rtype: string or None
1025
    @return: block device path if the volume is mapped, else None
1026

1027
    """
1028
    try:
1029
      devices = serializer.LoadJson(output)
1030
    except ValueError, err:
1031
      base.ThrowError("Unable to parse JSON data: %s" % err)
1032

    
1033
    rbd_dev = None
1034
    for d in devices.values(): # pylint: disable=E1103
1035
      try:
1036
        name = d["name"]
1037
      except KeyError:
1038
        base.ThrowError("'name' key missing from json object %s", devices)
1039

    
1040
      if name == volume_name:
1041
        if rbd_dev is not None:
1042
          base.ThrowError("rbd volume %s is mapped more than once", volume_name)
1043

    
1044
        rbd_dev = d["device"]
1045

    
1046
    return rbd_dev
1047

    
1048
  @staticmethod
1049
  def _ParseRbdShowmappedPlain(output, volume_name):
1050
    """Parse the (plain / text) output of `rbd showmapped'.
1051

1052
    This method parses the output of `rbd showmapped' and returns
1053
    the rbd block device path (e.g. /dev/rbd0) that matches the
1054
    given rbd volume.
1055

1056
    @type output: string
1057
    @param output: the plain text output of `rbd showmapped'
1058
    @type volume_name: string
1059
    @param volume_name: the name of the volume whose device we search for
1060
    @rtype: string or None
1061
    @return: block device path if the volume is mapped, else None
1062

1063
    """
1064
    allfields = 5
1065
    volumefield = 2
1066
    devicefield = 4
1067

    
1068
    lines = output.splitlines()
1069

    
1070
    # Try parsing the new output format (ceph >= 0.55).
1071
    splitted_lines = map(lambda l: l.split(), lines)
1072

    
1073
    # Check for empty output.
1074
    if not splitted_lines:
1075
      return None
1076

    
1077
    # Check showmapped output, to determine number of fields.
1078
    field_cnt = len(splitted_lines[0])
1079
    if field_cnt != allfields:
1080
      # Parsing the new format failed. Fallback to parsing the old output
1081
      # format (< 0.55).
1082
      splitted_lines = map(lambda l: l.split("\t"), lines)
1083
      if field_cnt != allfields:
1084
        base.ThrowError("Cannot parse rbd showmapped output expected %s fields,"
1085
                        " found %s", allfields, field_cnt)
1086

    
1087
    matched_lines = \
1088
      filter(lambda l: len(l) == allfields and l[volumefield] == volume_name,
1089
             splitted_lines)
1090

    
1091
    if len(matched_lines) > 1:
1092
      base.ThrowError("rbd volume %s mapped more than once", volume_name)
1093

    
1094
    if matched_lines:
1095
      # rbd block device found. Return it.
1096
      rbd_dev = matched_lines[0][devicefield]
1097
      return rbd_dev
1098

    
1099
    # The given volume is not mapped.
1100
    return None
1101

    
1102
  def Assemble(self):
1103
    """Assemble the device.
1104

1105
    """
1106
    pass
1107

    
1108
  def Shutdown(self):
1109
    """Shutdown the device.
1110

1111
    """
1112
    if not self.minor and not self.Attach():
1113
      # The rbd device doesn't exist.
1114
      return
1115

    
1116
    # Unmap the block device from the Volume.
1117
    self._UnmapVolumeFromBlockdev(self.unique_id)
1118

    
1119
    self.minor = None
1120
    self.dev_path = None
1121

    
1122
  def _UnmapVolumeFromBlockdev(self, unique_id):
1123
    """Unmaps the rbd device from the Volume it is mapped.
1124

1125
    Unmaps the rbd device from the Volume it was previously mapped to.
1126
    This method should be idempotent if the Volume isn't mapped.
1127

1128
    """
1129
    pool = self.params[constants.LDP_POOL]
1130
    name = unique_id[1]
1131

    
1132
    # Check if the mapping already exists.
1133
    rbd_dev = self._VolumeToBlockdev(pool, name)
1134

    
1135
    if rbd_dev:
1136
      # The mapping exists. Unmap the rbd device.
1137
      unmap_cmd = [constants.RBD_CMD, "unmap", "%s" % rbd_dev]
1138
      result = utils.RunCmd(unmap_cmd)
1139
      if result.failed:
1140
        base.ThrowError("rbd unmap failed (%s): %s",
1141
                        result.fail_reason, result.output)
1142

    
1143
  def Open(self, force=False):
1144
    """Make the device ready for I/O.
1145

1146
    """
1147
    pass
1148

    
1149
  def Close(self):
1150
    """Notifies that the device will no longer be used for I/O.
1151

1152
    """
1153
    pass
1154

    
1155
  def Grow(self, amount, dryrun, backingstore, excl_stor):
1156
    """Grow the Volume.
1157

1158
    @type amount: integer
1159
    @param amount: the amount (in mebibytes) to grow with
1160
    @type dryrun: boolean
1161
    @param dryrun: whether to execute the operation in simulation mode
1162
        only, without actually increasing the size
1163

1164
    """
1165
    if not backingstore:
1166
      return
1167
    if not self.Attach():
1168
      base.ThrowError("Can't attach to rbd device during Grow()")
1169

    
1170
    if dryrun:
1171
      # the rbd tool does not support dry runs of resize operations.
1172
      # Since rbd volumes are thinly provisioned, we assume
1173
      # there is always enough free space for the operation.
1174
      return
1175

    
1176
    rbd_pool = self.params[constants.LDP_POOL]
1177
    rbd_name = self.unique_id[1]
1178
    new_size = self.size + amount
1179

    
1180
    # Resize the rbd volume (Image) inside the RADOS cluster.
1181
    cmd = [constants.RBD_CMD, "resize", "-p", rbd_pool,
1182
           rbd_name, "--size", "%s" % new_size]
1183
    result = utils.RunCmd(cmd)
1184
    if result.failed:
1185
      base.ThrowError("rbd resize failed (%s): %s",
1186
                      result.fail_reason, result.output)
1187

    
1188
  def GetUserspaceAccessUri(self, hypervisor):
1189
    """Generate KVM userspace URIs to be used as `-drive file` settings.
1190

1191
    @see: L{BlockDev.GetUserspaceAccessUri}
1192

1193
    """
1194
    if hypervisor == constants.HT_KVM:
1195
      return "rbd:" + self.rbd_pool + "/" + self.rbd_name
1196
    else:
1197
      base.ThrowError("Hypervisor %s doesn't support RBD userspace access" %
1198
                      hypervisor)
1199

    
1200

    
1201
class ExtStorageDevice(base.BlockDev):
1202
  """A block device provided by an ExtStorage Provider.
1203

1204
  This class implements the External Storage Interface, which means
1205
  handling of the externally provided block devices.
1206

1207
  """
1208
  def __init__(self, unique_id, children, size, params, dyn_params):
1209
    """Attaches to an extstorage block device.
1210

1211
    """
1212
    super(ExtStorageDevice, self).__init__(unique_id, children, size, params,
1213
                                           dyn_params)
1214
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1215
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1216

    
1217
    self.driver, self.vol_name = unique_id
1218
    self.ext_params = params
1219

    
1220
    self.major = self.minor = None
1221
    self.Attach()
1222

    
1223
  @classmethod
1224
  def Create(cls, unique_id, children, size, spindles, params, excl_stor,
1225
             dyn_params):
1226
    """Create a new extstorage device.
1227

1228
    Provision a new volume using an extstorage provider, which will
1229
    then be mapped to a block device.
1230

1231
    """
1232
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1233
      raise errors.ProgrammerError("Invalid configuration data %s" %
1234
                                   str(unique_id))
1235
    if excl_stor:
1236
      raise errors.ProgrammerError("extstorage device requested with"
1237
                                   " exclusive_storage")
1238

    
1239
    # Call the External Storage's create script,
1240
    # to provision a new Volume inside the External Storage
1241
    _ExtStorageAction(constants.ES_ACTION_CREATE, unique_id,
1242
                      params, str(size))
1243

    
1244
    return ExtStorageDevice(unique_id, children, size, params, dyn_params)
1245

    
1246
  def Remove(self):
1247
    """Remove the extstorage device.
1248

1249
    """
1250
    if not self.minor and not self.Attach():
1251
      # The extstorage device doesn't exist.
1252
      return
1253

    
1254
    # First shutdown the device (remove mappings).
1255
    self.Shutdown()
1256

    
1257
    # Call the External Storage's remove script,
1258
    # to remove the Volume from the External Storage
1259
    _ExtStorageAction(constants.ES_ACTION_REMOVE, self.unique_id,
1260
                      self.ext_params)
1261

    
1262
  def Rename(self, new_id):
1263
    """Rename this device.
1264

1265
    """
1266
    pass
1267

    
1268
  def Attach(self):
1269
    """Attach to an existing extstorage device.
1270

1271
    This method maps the extstorage volume that matches our name with
1272
    a corresponding block device and then attaches to this device.
1273

1274
    """
1275
    self.attached = False
1276

    
1277
    # Call the External Storage's attach script,
1278
    # to attach an existing Volume to a block device under /dev
1279
    self.dev_path = _ExtStorageAction(constants.ES_ACTION_ATTACH,
1280
                                      self.unique_id, self.ext_params)
1281

    
1282
    try:
1283
      st = os.stat(self.dev_path)
1284
    except OSError, err:
1285
      logging.error("Error stat()'ing %s: %s", self.dev_path, str(err))
1286
      return False
1287

    
1288
    if not stat.S_ISBLK(st.st_mode):
1289
      logging.error("%s is not a block device", self.dev_path)
1290
      return False
1291

    
1292
    self.major = os.major(st.st_rdev)
1293
    self.minor = os.minor(st.st_rdev)
1294
    self.attached = True
1295

    
1296
    return True
1297

    
1298
  def Assemble(self):
1299
    """Assemble the device.
1300

1301
    """
1302
    pass
1303

    
1304
  def Shutdown(self):
1305
    """Shutdown the device.
1306

1307
    """
1308
    if not self.minor and not self.Attach():
1309
      # The extstorage device doesn't exist.
1310
      return
1311

    
1312
    # Call the External Storage's detach script,
1313
    # to detach an existing Volume from it's block device under /dev
1314
    _ExtStorageAction(constants.ES_ACTION_DETACH, self.unique_id,
1315
                      self.ext_params)
1316

    
1317
    self.minor = None
1318
    self.dev_path = None
1319

    
1320
  def Open(self, force=False):
1321
    """Make the device ready for I/O.
1322

1323
    """
1324
    pass
1325

    
1326
  def Close(self):
1327
    """Notifies that the device will no longer be used for I/O.
1328

1329
    """
1330
    pass
1331

    
1332
  def Grow(self, amount, dryrun, backingstore, excl_stor):
1333
    """Grow the Volume.
1334

1335
    @type amount: integer
1336
    @param amount: the amount (in mebibytes) to grow with
1337
    @type dryrun: boolean
1338
    @param dryrun: whether to execute the operation in simulation mode
1339
        only, without actually increasing the size
1340

1341
    """
1342
    if not backingstore:
1343
      return
1344
    if not self.Attach():
1345
      base.ThrowError("Can't attach to extstorage device during Grow()")
1346

    
1347
    if dryrun:
1348
      # we do not support dry runs of resize operations for now.
1349
      return
1350

    
1351
    new_size = self.size + amount
1352

    
1353
    # Call the External Storage's grow script,
1354
    # to grow an existing Volume inside the External Storage
1355
    _ExtStorageAction(constants.ES_ACTION_GROW, self.unique_id,
1356
                      self.ext_params, str(self.size), grow=str(new_size))
1357

    
1358
  def SetInfo(self, text):
1359
    """Update metadata with info text.
1360

1361
    """
1362
    # Replace invalid characters
1363
    text = re.sub("^[^A-Za-z0-9_+.]", "_", text)
1364
    text = re.sub("[^-A-Za-z0-9_+.]", "_", text)
1365

    
1366
    # Only up to 128 characters are allowed
1367
    text = text[:128]
1368

    
1369
    # Call the External Storage's setinfo script,
1370
    # to set metadata for an existing Volume inside the External Storage
1371
    _ExtStorageAction(constants.ES_ACTION_SETINFO, self.unique_id,
1372
                      self.ext_params, metadata=text)
1373

    
1374

    
1375
def _ExtStorageAction(action, unique_id, ext_params,
1376
                      size=None, grow=None, metadata=None):
1377
  """Take an External Storage action.
1378

1379
  Take an External Storage action concerning or affecting
1380
  a specific Volume inside the External Storage.
1381

1382
  @type action: string
1383
  @param action: which action to perform. One of:
1384
                 create / remove / grow / attach / detach
1385
  @type unique_id: tuple (driver, vol_name)
1386
  @param unique_id: a tuple containing the type of ExtStorage (driver)
1387
                    and the Volume name
1388
  @type ext_params: dict
1389
  @param ext_params: ExtStorage parameters
1390
  @type size: integer
1391
  @param size: the size of the Volume in mebibytes
1392
  @type grow: integer
1393
  @param grow: the new size in mebibytes (after grow)
1394
  @type metadata: string
1395
  @param metadata: metadata info of the Volume, for use by the provider
1396
  @rtype: None or a block device path (during attach)
1397

1398
  """
1399
  driver, vol_name = unique_id
1400

    
1401
  # Create an External Storage instance of type `driver'
1402
  status, inst_es = ExtStorageFromDisk(driver)
1403
  if not status:
1404
    base.ThrowError("%s" % inst_es)
1405

    
1406
  # Create the basic environment for the driver's scripts
1407
  create_env = _ExtStorageEnvironment(unique_id, ext_params, size,
1408
                                      grow, metadata)
1409

    
1410
  # Do not use log file for action `attach' as we need
1411
  # to get the output from RunResult
1412
  # TODO: find a way to have a log file for attach too
1413
  logfile = None
1414
  if action is not constants.ES_ACTION_ATTACH:
1415
    logfile = _VolumeLogName(action, driver, vol_name)
1416

    
1417
  # Make sure the given action results in a valid script
1418
  if action not in constants.ES_SCRIPTS:
1419
    base.ThrowError("Action '%s' doesn't result in a valid ExtStorage script" %
1420
                    action)
1421

    
1422
  # Find out which external script to run according the given action
1423
  script_name = action + "_script"
1424
  script = getattr(inst_es, script_name)
1425

    
1426
  # Run the external script
1427
  result = utils.RunCmd([script], env=create_env,
1428
                        cwd=inst_es.path, output=logfile,)
1429
  if result.failed:
1430
    logging.error("External storage's %s command '%s' returned"
1431
                  " error: %s, logfile: %s, output: %s",
1432
                  action, result.cmd, result.fail_reason,
1433
                  logfile, result.output)
1434

    
1435
    # If logfile is 'None' (during attach), it breaks TailFile
1436
    # TODO: have a log file for attach too
1437
    if action is not constants.ES_ACTION_ATTACH:
1438
      lines = [utils.SafeEncode(val)
1439
               for val in utils.TailFile(logfile, lines=20)]
1440
    else:
1441
      lines = result.output[-20:]
1442

    
1443
    base.ThrowError("External storage's %s script failed (%s), last"
1444
                    " lines of output:\n%s",
1445
                    action, result.fail_reason, "\n".join(lines))
1446

    
1447
  if action == constants.ES_ACTION_ATTACH:
1448
    return result.stdout
1449

    
1450

    
1451
def ExtStorageFromDisk(name, base_dir=None):
1452
  """Create an ExtStorage instance from disk.
1453

1454
  This function will return an ExtStorage instance
1455
  if the given name is a valid ExtStorage name.
1456

1457
  @type base_dir: string
1458
  @keyword base_dir: Base directory containing ExtStorage installations.
1459
                     Defaults to a search in all the ES_SEARCH_PATH dirs.
1460
  @rtype: tuple
1461
  @return: True and the ExtStorage instance if we find a valid one, or
1462
      False and the diagnose message on error
1463

1464
  """
1465
  if base_dir is None:
1466
    es_base_dir = pathutils.ES_SEARCH_PATH
1467
  else:
1468
    es_base_dir = [base_dir]
1469

    
1470
  es_dir = utils.FindFile(name, es_base_dir, os.path.isdir)
1471

    
1472
  if es_dir is None:
1473
    return False, ("Directory for External Storage Provider %s not"
1474
                   " found in search path" % name)
1475

    
1476
  # ES Files dictionary, we will populate it with the absolute path
1477
  # names; if the value is True, then it is a required file, otherwise
1478
  # an optional one
1479
  es_files = dict.fromkeys(constants.ES_SCRIPTS, True)
1480

    
1481
  es_files[constants.ES_PARAMETERS_FILE] = True
1482

    
1483
  for (filename, _) in es_files.items():
1484
    es_files[filename] = utils.PathJoin(es_dir, filename)
1485

    
1486
    try:
1487
      st = os.stat(es_files[filename])
1488
    except EnvironmentError, err:
1489
      return False, ("File '%s' under path '%s' is missing (%s)" %
1490
                     (filename, es_dir, utils.ErrnoOrStr(err)))
1491

    
1492
    if not stat.S_ISREG(stat.S_IFMT(st.st_mode)):
1493
      return False, ("File '%s' under path '%s' is not a regular file" %
1494
                     (filename, es_dir))
1495

    
1496
    if filename in constants.ES_SCRIPTS:
1497
      if stat.S_IMODE(st.st_mode) & stat.S_IXUSR != stat.S_IXUSR:
1498
        return False, ("File '%s' under path '%s' is not executable" %
1499
                       (filename, es_dir))
1500

    
1501
  parameters = []
1502
  if constants.ES_PARAMETERS_FILE in es_files:
1503
    parameters_file = es_files[constants.ES_PARAMETERS_FILE]
1504
    try:
1505
      parameters = utils.ReadFile(parameters_file).splitlines()
1506
    except EnvironmentError, err:
1507
      return False, ("Error while reading the EXT parameters file at %s: %s" %
1508
                     (parameters_file, utils.ErrnoOrStr(err)))
1509
    parameters = [v.split(None, 1) for v in parameters]
1510

    
1511
  es_obj = \
1512
    objects.ExtStorage(name=name, path=es_dir,
1513
                       create_script=es_files[constants.ES_SCRIPT_CREATE],
1514
                       remove_script=es_files[constants.ES_SCRIPT_REMOVE],
1515
                       grow_script=es_files[constants.ES_SCRIPT_GROW],
1516
                       attach_script=es_files[constants.ES_SCRIPT_ATTACH],
1517
                       detach_script=es_files[constants.ES_SCRIPT_DETACH],
1518
                       setinfo_script=es_files[constants.ES_SCRIPT_SETINFO],
1519
                       verify_script=es_files[constants.ES_SCRIPT_VERIFY],
1520
                       supported_parameters=parameters)
1521
  return True, es_obj
1522

    
1523

    
1524
def _ExtStorageEnvironment(unique_id, ext_params,
1525
                           size=None, grow=None, metadata=None):
1526
  """Calculate the environment for an External Storage script.
1527

1528
  @type unique_id: tuple (driver, vol_name)
1529
  @param unique_id: ExtStorage pool and name of the Volume
1530
  @type ext_params: dict
1531
  @param ext_params: the EXT parameters
1532
  @type size: string
1533
  @param size: size of the Volume (in mebibytes)
1534
  @type grow: string
1535
  @param grow: new size of Volume after grow (in mebibytes)
1536
  @type metadata: string
1537
  @param metadata: metadata info of the Volume
1538
  @rtype: dict
1539
  @return: dict of environment variables
1540

1541
  """
1542
  vol_name = unique_id[1]
1543

    
1544
  result = {}
1545
  result["VOL_NAME"] = vol_name
1546

    
1547
  # EXT params
1548
  for pname, pvalue in ext_params.items():
1549
    result["EXTP_%s" % pname.upper()] = str(pvalue)
1550

    
1551
  if size is not None:
1552
    result["VOL_SIZE"] = size
1553

    
1554
  if grow is not None:
1555
    result["VOL_NEW_SIZE"] = grow
1556

    
1557
  if metadata is not None:
1558
    result["VOL_METADATA"] = metadata
1559

    
1560
  return result
1561

    
1562

    
1563
def _VolumeLogName(kind, es_name, volume):
1564
  """Compute the ExtStorage log filename for a given Volume and operation.
1565

1566
  @type kind: string
1567
  @param kind: the operation type (e.g. create, remove etc.)
1568
  @type es_name: string
1569
  @param es_name: the ExtStorage name
1570
  @type volume: string
1571
  @param volume: the name of the Volume inside the External Storage
1572

1573
  """
1574
  # Check if the extstorage log dir is a valid dir
1575
  if not os.path.isdir(pathutils.LOG_ES_DIR):
1576
    base.ThrowError("Cannot find log directory: %s", pathutils.LOG_ES_DIR)
1577

    
1578
  # TODO: Use tempfile.mkstemp to create unique filename
1579
  basename = ("%s-%s-%s-%s.log" %
1580
              (kind, es_name, volume, utils.TimestampForFilename()))
1581
  return utils.PathJoin(pathutils.LOG_ES_DIR, basename)
1582

    
1583

    
1584
def _VerifyDiskType(dev_type):
1585
  if dev_type not in DEV_MAP:
1586
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1587

    
1588

    
1589
def _VerifyDiskParams(disk):
1590
  """Verifies if all disk parameters are set.
1591

1592
  """
1593
  missing = set(constants.DISK_LD_DEFAULTS[disk.dev_type]) - set(disk.params)
1594
  if missing:
1595
    raise errors.ProgrammerError("Block device is missing disk parameters: %s" %
1596
                                 missing)
1597

    
1598

    
1599
def FindDevice(disk, children):
1600
  """Search for an existing, assembled device.
1601

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

1605
  @type disk: L{objects.Disk}
1606
  @param disk: the disk object to find
1607
  @type children: list of L{bdev.BlockDev}
1608
  @param children: the list of block devices that are children of the device
1609
                  represented by the disk parameter
1610

1611
  """
1612
  _VerifyDiskType(disk.dev_type)
1613
  device = DEV_MAP[disk.dev_type](disk.logical_id, children, disk.size,
1614
                                  disk.params, disk.dynamic_params)
1615
  if not device.attached:
1616
    return None
1617
  return device
1618

    
1619

    
1620
def Assemble(disk, children):
1621
  """Try to attach or assemble an existing device.
1622

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

1626
  @type disk: L{objects.Disk}
1627
  @param disk: the disk object to assemble
1628
  @type children: list of L{bdev.BlockDev}
1629
  @param children: the list of block devices that are children of the device
1630
                  represented by the disk parameter
1631

1632
  """
1633
  _VerifyDiskType(disk.dev_type)
1634
  _VerifyDiskParams(disk)
1635
  device = DEV_MAP[disk.dev_type](disk.logical_id, children, disk.size,
1636
                                  disk.params, disk.dynamic_params)
1637
  device.Assemble()
1638
  return device
1639

    
1640

    
1641
def Create(disk, children, excl_stor):
1642
  """Create a device.
1643

1644
  @type disk: L{objects.Disk}
1645
  @param disk: the disk object to create
1646
  @type children: list of L{bdev.BlockDev}
1647
  @param children: the list of block devices that are children of the device
1648
                  represented by the disk parameter
1649
  @type excl_stor: boolean
1650
  @param excl_stor: Whether exclusive_storage is active
1651
  @rtype: L{bdev.BlockDev}
1652
  @return: the created device, or C{None} in case of an error
1653

1654
  """
1655
  _VerifyDiskType(disk.dev_type)
1656
  _VerifyDiskParams(disk)
1657
  device = DEV_MAP[disk.dev_type].Create(disk.logical_id, children, disk.size,
1658
                                         disk.spindles, disk.params, excl_stor,
1659
                                         disk.dynamic_params)
1660
  return device
1661

    
1662
# Please keep this at the bottom of the file for visibility.
1663
DEV_MAP = {
1664
  constants.DT_PLAIN: LogicalVolume,
1665
  constants.DT_DRBD8: drbd.DRBD8Dev,
1666
  constants.DT_BLOCK: PersistentBlockDevice,
1667
  constants.DT_RBD: RADOSBlockDevice,
1668
  constants.DT_EXT: ExtStorageDevice,
1669
  constants.DT_FILE: FileStorage,
1670
  constants.DT_SHARED_FILE: FileStorage,
1671
  constants.DT_GLUSTER: GlusterStorage,
1672
}
1673
"""Map disk types to disk type classes.
1674

1675
@see: L{Assemble}, L{FindDevice}, L{Create}.""" # pylint: disable=W0105