Statistics
| Branch: | Tag: | Revision:

root / lib / storage / bdev.py @ 0c3d9c7c

History | View | Annotate | Download (57 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):
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)
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 the 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):
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):
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)
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):
1000
    """Attaches to an rbd device.
1001

1002
    """
1003
    super(RADOSBlockDevice, self).__init__(unique_id, children, size, params,
1004
                                           dyn_params)
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

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

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

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

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

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

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

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

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

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

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

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

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

1064
    """
1065
    pass
1066

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

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

1073
    """
1074
    self.attached = False
1075

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

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

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

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

    
1093
    return True
1094

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1202
        rbd_dev = d["device"]
1203

    
1204
    return rbd_dev
1205

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

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

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

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

    
1226
    lines = output.splitlines()
1227

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

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

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

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

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

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

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

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

1263
    """
1264
    pass
1265

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

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

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

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

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

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

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

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

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

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

1304
    """
1305
    pass
1306

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

1310
    """
1311
    pass
1312

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

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

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

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

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

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

    
1346

    
1347
class ExtStorageDevice(base.BlockDev):
1348
  """A block device provided by an ExtStorage Provider.
1349

1350
  This class implements the External Storage Interface, which means
1351
  handling of the externally provided block devices.
1352

1353
  """
1354
  def __init__(self, unique_id, children, size, params, dyn_params):
1355
    """Attaches to an extstorage block device.
1356

1357
    """
1358
    super(ExtStorageDevice, self).__init__(unique_id, children, size, params,
1359
                                           dyn_params)
1360
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1361
      raise ValueError("Invalid configuration data %s" % str(unique_id))
1362

    
1363
    self.driver, self.vol_name = unique_id
1364
    self.ext_params = params
1365

    
1366
    self.major = self.minor = None
1367
    self.Attach()
1368

    
1369
  @classmethod
1370
  def Create(cls, unique_id, children, size, spindles, params, excl_stor,
1371
             dyn_params):
1372
    """Create a new extstorage device.
1373

1374
    Provision a new volume using an extstorage provider, which will
1375
    then be mapped to a block device.
1376

1377
    """
1378
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1379
      raise errors.ProgrammerError("Invalid configuration data %s" %
1380
                                   str(unique_id))
1381
    if excl_stor:
1382
      raise errors.ProgrammerError("extstorage device requested with"
1383
                                   " exclusive_storage")
1384

    
1385
    # Call the External Storage's create script,
1386
    # to provision a new Volume inside the External Storage
1387
    _ExtStorageAction(constants.ES_ACTION_CREATE, unique_id,
1388
                      params, str(size))
1389

    
1390
    return ExtStorageDevice(unique_id, children, size, params, dyn_params)
1391

    
1392
  def Remove(self):
1393
    """Remove the extstorage device.
1394

1395
    """
1396
    if not self.minor and not self.Attach():
1397
      # The extstorage device doesn't exist.
1398
      return
1399

    
1400
    # First shutdown the device (remove mappings).
1401
    self.Shutdown()
1402

    
1403
    # Call the External Storage's remove script,
1404
    # to remove the Volume from the External Storage
1405
    _ExtStorageAction(constants.ES_ACTION_REMOVE, self.unique_id,
1406
                      self.ext_params)
1407

    
1408
  def Rename(self, new_id):
1409
    """Rename this device.
1410

1411
    """
1412
    pass
1413

    
1414
  def Attach(self):
1415
    """Attach to an existing extstorage device.
1416

1417
    This method maps the extstorage volume that matches our name with
1418
    a corresponding block device and then attaches to this device.
1419

1420
    """
1421
    self.attached = False
1422

    
1423
    # Call the External Storage's attach script,
1424
    # to attach an existing Volume to a block device under /dev
1425
    self.dev_path = _ExtStorageAction(constants.ES_ACTION_ATTACH,
1426
                                      self.unique_id, self.ext_params)
1427

    
1428
    try:
1429
      st = os.stat(self.dev_path)
1430
    except OSError, err:
1431
      logging.error("Error stat()'ing %s: %s", self.dev_path, str(err))
1432
      return False
1433

    
1434
    if not stat.S_ISBLK(st.st_mode):
1435
      logging.error("%s is not a block device", self.dev_path)
1436
      return False
1437

    
1438
    self.major = os.major(st.st_rdev)
1439
    self.minor = os.minor(st.st_rdev)
1440
    self.attached = True
1441

    
1442
    return True
1443

    
1444
  def Assemble(self):
1445
    """Assemble the device.
1446

1447
    """
1448
    pass
1449

    
1450
  def Shutdown(self):
1451
    """Shutdown the device.
1452

1453
    """
1454
    if not self.minor and not self.Attach():
1455
      # The extstorage device doesn't exist.
1456
      return
1457

    
1458
    # Call the External Storage's detach script,
1459
    # to detach an existing Volume from it's block device under /dev
1460
    _ExtStorageAction(constants.ES_ACTION_DETACH, self.unique_id,
1461
                      self.ext_params)
1462

    
1463
    self.minor = None
1464
    self.dev_path = None
1465

    
1466
  def Open(self, force=False):
1467
    """Make the device ready for I/O.
1468

1469
    """
1470
    pass
1471

    
1472
  def Close(self):
1473
    """Notifies that the device will no longer be used for I/O.
1474

1475
    """
1476
    pass
1477

    
1478
  def Grow(self, amount, dryrun, backingstore, excl_stor):
1479
    """Grow the Volume.
1480

1481
    @type amount: integer
1482
    @param amount: the amount (in mebibytes) to grow with
1483
    @type dryrun: boolean
1484
    @param dryrun: whether to execute the operation in simulation mode
1485
        only, without actually increasing the size
1486

1487
    """
1488
    if not backingstore:
1489
      return
1490
    if not self.Attach():
1491
      base.ThrowError("Can't attach to extstorage device during Grow()")
1492

    
1493
    if dryrun:
1494
      # we do not support dry runs of resize operations for now.
1495
      return
1496

    
1497
    new_size = self.size + amount
1498

    
1499
    # Call the External Storage's grow script,
1500
    # to grow an existing Volume inside the External Storage
1501
    _ExtStorageAction(constants.ES_ACTION_GROW, self.unique_id,
1502
                      self.ext_params, str(self.size), grow=str(new_size))
1503

    
1504
  def SetInfo(self, text):
1505
    """Update metadata with info text.
1506

1507
    """
1508
    # Replace invalid characters
1509
    text = re.sub("^[^A-Za-z0-9_+.]", "_", text)
1510
    text = re.sub("[^-A-Za-z0-9_+.]", "_", text)
1511

    
1512
    # Only up to 128 characters are allowed
1513
    text = text[:128]
1514

    
1515
    # Call the External Storage's setinfo script,
1516
    # to set metadata for an existing Volume inside the External Storage
1517
    _ExtStorageAction(constants.ES_ACTION_SETINFO, self.unique_id,
1518
                      self.ext_params, metadata=text)
1519

    
1520

    
1521
def _ExtStorageAction(action, unique_id, ext_params,
1522
                      size=None, grow=None, metadata=None):
1523
  """Take an External Storage action.
1524

1525
  Take an External Storage action concerning or affecting
1526
  a specific Volume inside the External Storage.
1527

1528
  @type action: string
1529
  @param action: which action to perform. One of:
1530
                 create / remove / grow / attach / detach
1531
  @type unique_id: tuple (driver, vol_name)
1532
  @param unique_id: a tuple containing the type of ExtStorage (driver)
1533
                    and the Volume name
1534
  @type ext_params: dict
1535
  @param ext_params: ExtStorage parameters
1536
  @type size: integer
1537
  @param size: the size of the Volume in mebibytes
1538
  @type grow: integer
1539
  @param grow: the new size in mebibytes (after grow)
1540
  @type metadata: string
1541
  @param metadata: metadata info of the Volume, for use by the provider
1542
  @rtype: None or a block device path (during attach)
1543

1544
  """
1545
  driver, vol_name = unique_id
1546

    
1547
  # Create an External Storage instance of type `driver'
1548
  status, inst_es = ExtStorageFromDisk(driver)
1549
  if not status:
1550
    base.ThrowError("%s" % inst_es)
1551

    
1552
  # Create the basic environment for the driver's scripts
1553
  create_env = _ExtStorageEnvironment(unique_id, ext_params, size,
1554
                                      grow, metadata)
1555

    
1556
  # Do not use log file for action `attach' as we need
1557
  # to get the output from RunResult
1558
  # TODO: find a way to have a log file for attach too
1559
  logfile = None
1560
  if action is not constants.ES_ACTION_ATTACH:
1561
    logfile = _VolumeLogName(action, driver, vol_name)
1562

    
1563
  # Make sure the given action results in a valid script
1564
  if action not in constants.ES_SCRIPTS:
1565
    base.ThrowError("Action '%s' doesn't result in a valid ExtStorage script" %
1566
                    action)
1567

    
1568
  # Find out which external script to run according the given action
1569
  script_name = action + "_script"
1570
  script = getattr(inst_es, script_name)
1571

    
1572
  # Run the external script
1573
  result = utils.RunCmd([script], env=create_env,
1574
                        cwd=inst_es.path, output=logfile,)
1575
  if result.failed:
1576
    logging.error("External storage's %s command '%s' returned"
1577
                  " error: %s, logfile: %s, output: %s",
1578
                  action, result.cmd, result.fail_reason,
1579
                  logfile, result.output)
1580

    
1581
    # If logfile is 'None' (during attach), it breaks TailFile
1582
    # TODO: have a log file for attach too
1583
    if action is not constants.ES_ACTION_ATTACH:
1584
      lines = [utils.SafeEncode(val)
1585
               for val in utils.TailFile(logfile, lines=20)]
1586
    else:
1587
      lines = result.output[-20:]
1588

    
1589
    base.ThrowError("External storage's %s script failed (%s), last"
1590
                    " lines of output:\n%s",
1591
                    action, result.fail_reason, "\n".join(lines))
1592

    
1593
  if action == constants.ES_ACTION_ATTACH:
1594
    return result.stdout
1595

    
1596

    
1597
def ExtStorageFromDisk(name, base_dir=None):
1598
  """Create an ExtStorage instance from disk.
1599

1600
  This function will return an ExtStorage instance
1601
  if the given name is a valid ExtStorage name.
1602

1603
  @type base_dir: string
1604
  @keyword base_dir: Base directory containing ExtStorage installations.
1605
                     Defaults to a search in all the ES_SEARCH_PATH dirs.
1606
  @rtype: tuple
1607
  @return: True and the ExtStorage instance if we find a valid one, or
1608
      False and the diagnose message on error
1609

1610
  """
1611
  if base_dir is None:
1612
    es_base_dir = pathutils.ES_SEARCH_PATH
1613
  else:
1614
    es_base_dir = [base_dir]
1615

    
1616
  es_dir = utils.FindFile(name, es_base_dir, os.path.isdir)
1617

    
1618
  if es_dir is None:
1619
    return False, ("Directory for External Storage Provider %s not"
1620
                   " found in search path" % name)
1621

    
1622
  # ES Files dictionary, we will populate it with the absolute path
1623
  # names; if the value is True, then it is a required file, otherwise
1624
  # an optional one
1625
  es_files = dict.fromkeys(constants.ES_SCRIPTS, True)
1626

    
1627
  es_files[constants.ES_PARAMETERS_FILE] = True
1628

    
1629
  for (filename, _) in es_files.items():
1630
    es_files[filename] = utils.PathJoin(es_dir, filename)
1631

    
1632
    try:
1633
      st = os.stat(es_files[filename])
1634
    except EnvironmentError, err:
1635
      return False, ("File '%s' under path '%s' is missing (%s)" %
1636
                     (filename, es_dir, utils.ErrnoOrStr(err)))
1637

    
1638
    if not stat.S_ISREG(stat.S_IFMT(st.st_mode)):
1639
      return False, ("File '%s' under path '%s' is not a regular file" %
1640
                     (filename, es_dir))
1641

    
1642
    if filename in constants.ES_SCRIPTS:
1643
      if stat.S_IMODE(st.st_mode) & stat.S_IXUSR != stat.S_IXUSR:
1644
        return False, ("File '%s' under path '%s' is not executable" %
1645
                       (filename, es_dir))
1646

    
1647
  parameters = []
1648
  if constants.ES_PARAMETERS_FILE in es_files:
1649
    parameters_file = es_files[constants.ES_PARAMETERS_FILE]
1650
    try:
1651
      parameters = utils.ReadFile(parameters_file).splitlines()
1652
    except EnvironmentError, err:
1653
      return False, ("Error while reading the EXT parameters file at %s: %s" %
1654
                     (parameters_file, utils.ErrnoOrStr(err)))
1655
    parameters = [v.split(None, 1) for v in parameters]
1656

    
1657
  es_obj = \
1658
    objects.ExtStorage(name=name, path=es_dir,
1659
                       create_script=es_files[constants.ES_SCRIPT_CREATE],
1660
                       remove_script=es_files[constants.ES_SCRIPT_REMOVE],
1661
                       grow_script=es_files[constants.ES_SCRIPT_GROW],
1662
                       attach_script=es_files[constants.ES_SCRIPT_ATTACH],
1663
                       detach_script=es_files[constants.ES_SCRIPT_DETACH],
1664
                       setinfo_script=es_files[constants.ES_SCRIPT_SETINFO],
1665
                       verify_script=es_files[constants.ES_SCRIPT_VERIFY],
1666
                       supported_parameters=parameters)
1667
  return True, es_obj
1668

    
1669

    
1670
def _ExtStorageEnvironment(unique_id, ext_params,
1671
                           size=None, grow=None, metadata=None):
1672
  """Calculate the environment for an External Storage script.
1673

1674
  @type unique_id: tuple (driver, vol_name)
1675
  @param unique_id: ExtStorage pool and name of the Volume
1676
  @type ext_params: dict
1677
  @param ext_params: the EXT parameters
1678
  @type size: string
1679
  @param size: size of the Volume (in mebibytes)
1680
  @type grow: string
1681
  @param grow: new size of Volume after grow (in mebibytes)
1682
  @type metadata: string
1683
  @param metadata: metadata info of the Volume
1684
  @rtype: dict
1685
  @return: dict of environment variables
1686

1687
  """
1688
  vol_name = unique_id[1]
1689

    
1690
  result = {}
1691
  result["VOL_NAME"] = vol_name
1692

    
1693
  # EXT params
1694
  for pname, pvalue in ext_params.items():
1695
    result["EXTP_%s" % pname.upper()] = str(pvalue)
1696

    
1697
  if size is not None:
1698
    result["VOL_SIZE"] = size
1699

    
1700
  if grow is not None:
1701
    result["VOL_NEW_SIZE"] = grow
1702

    
1703
  if metadata is not None:
1704
    result["VOL_METADATA"] = metadata
1705

    
1706
  return result
1707

    
1708

    
1709
def _VolumeLogName(kind, es_name, volume):
1710
  """Compute the ExtStorage log filename for a given Volume and operation.
1711

1712
  @type kind: string
1713
  @param kind: the operation type (e.g. create, remove etc.)
1714
  @type es_name: string
1715
  @param es_name: the ExtStorage name
1716
  @type volume: string
1717
  @param volume: the name of the Volume inside the External Storage
1718

1719
  """
1720
  # Check if the extstorage log dir is a valid dir
1721
  if not os.path.isdir(pathutils.LOG_ES_DIR):
1722
    base.ThrowError("Cannot find log directory: %s", pathutils.LOG_ES_DIR)
1723

    
1724
  # TODO: Use tempfile.mkstemp to create unique filename
1725
  basename = ("%s-%s-%s-%s.log" %
1726
              (kind, es_name, volume, utils.TimestampForFilename()))
1727
  return utils.PathJoin(pathutils.LOG_ES_DIR, basename)
1728

    
1729

    
1730
DEV_MAP = {
1731
  constants.DT_PLAIN: LogicalVolume,
1732
  constants.DT_DRBD8: drbd.DRBD8Dev,
1733
  constants.DT_BLOCK: PersistentBlockDevice,
1734
  constants.DT_RBD: RADOSBlockDevice,
1735
  constants.DT_EXT: ExtStorageDevice,
1736
  constants.DT_FILE: FileStorage,
1737
  constants.DT_SHARED_FILE: FileStorage,
1738
  }
1739

    
1740

    
1741
def _VerifyDiskType(dev_type):
1742
  if dev_type not in DEV_MAP:
1743
    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1744

    
1745

    
1746
def _VerifyDiskParams(disk):
1747
  """Verifies if all disk parameters are set.
1748

1749
  """
1750
  missing = set(constants.DISK_LD_DEFAULTS[disk.dev_type]) - set(disk.params)
1751
  if missing:
1752
    raise errors.ProgrammerError("Block device is missing disk parameters: %s" %
1753
                                 missing)
1754

    
1755

    
1756
def FindDevice(disk, children):
1757
  """Search for an existing, assembled device.
1758

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

1762
  @type disk: L{objects.Disk}
1763
  @param disk: the disk object to find
1764
  @type children: list of L{bdev.BlockDev}
1765
  @param children: the list of block devices that are children of the device
1766
                  represented by the disk parameter
1767

1768
  """
1769
  _VerifyDiskType(disk.dev_type)
1770
  device = DEV_MAP[disk.dev_type](disk.logical_id, children, disk.size,
1771
                                  disk.params, disk.dynamic_params)
1772
  if not device.attached:
1773
    return None
1774
  return device
1775

    
1776

    
1777
def Assemble(disk, children):
1778
  """Try to attach or assemble an existing device.
1779

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

1783
  @type disk: L{objects.Disk}
1784
  @param disk: the disk object to assemble
1785
  @type children: list of L{bdev.BlockDev}
1786
  @param children: the list of block devices that are children of the device
1787
                  represented by the disk parameter
1788

1789
  """
1790
  _VerifyDiskType(disk.dev_type)
1791
  _VerifyDiskParams(disk)
1792
  device = DEV_MAP[disk.dev_type](disk.logical_id, children, disk.size,
1793
                                  disk.params, disk.dynamic_params)
1794
  device.Assemble()
1795
  return device
1796

    
1797

    
1798
def Create(disk, children, excl_stor):
1799
  """Create a device.
1800

1801
  @type disk: L{objects.Disk}
1802
  @param disk: the disk object to create
1803
  @type children: list of L{bdev.BlockDev}
1804
  @param children: the list of block devices that are children of the device
1805
                  represented by the disk parameter
1806
  @type excl_stor: boolean
1807
  @param excl_stor: Whether exclusive_storage is active
1808
  @rtype: L{bdev.BlockDev}
1809
  @return: the created device, or C{None} in case of an error
1810

1811
  """
1812
  _VerifyDiskType(disk.dev_type)
1813
  _VerifyDiskParams(disk)
1814
  device = DEV_MAP[disk.dev_type].Create(disk.logical_id, children, disk.size,
1815
                                         disk.spindles, disk.params, excl_stor,
1816
                                         disk.dynamic_params)
1817
  return device