Statistics
| Branch: | Tag: | Revision:

root / lib / storage / bdev.py @ 78f99abb

History | View | Annotate | Download (56.4 kB)

1
#
2
#
3

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

    
21

    
22
"""Block device abstraction"""
23

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

    
31
from ganeti import utils
32
from ganeti import errors
33
from ganeti import constants
34
from ganeti import objects
35
from ganeti import compat
36
from ganeti import pathutils
37
from ganeti import serializer
38
from ganeti.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):
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
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
78
      raise ValueError("Invalid configuration data %s" % str(unique_id))
79
    self._vg_name, self._lv_name = unique_id
80
    self._ValidateName(self._vg_name)
81
    self._ValidateName(self._lv_name)
82
    self.dev_path = utils.PathJoin("/dev", self._vg_name, self._lv_name)
83
    self._degraded = True
84
    self.major = self.minor = self.pe_size = self.stripe_count = None
85
    self.pv_names = None
86
    self.Attach()
87

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

218
    """
219
    if not fields:
220
      raise errors.ProgrammerError("No fields specified")
221

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

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

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

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

    
239
      data.append(splitted_fields)
240

    
241
    return data
242

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

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

251
    @rtype: list
252
    @return: list of objects.LvmPvInfo objects
253

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

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

    
298
    return data
299

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

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

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

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

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

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

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

341
    """
342
    (_, free_pvs, num_pvs) = cls._GetRawFreePvInfo(vg_name)
343
    return (free_pvs, num_pvs)
344

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

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

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

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

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

    
380
    return data
381

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

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

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

    
396
  def Remove(self):
397
    """Remove this logical volume.
398

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

    
409
  def Rename(self, new_id):
410
    """Rename this logical volume.
411

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

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

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

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

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

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

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

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

    
463
    return (status, major, minor, pe_size, stripes, pv_names)
464

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

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

    
494
  def Attach(self):
495
    """Attach to an existing LV.
496

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

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

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

    
519
  def Assemble(self):
520
    """Assemble the device.
521

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

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

    
531
  def Shutdown(self):
532
    """Shutdown the device.
533

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

537
    """
538
    pass
539

    
540
  def GetSyncStatus(self):
541
    """Returns the sync status of the device.
542

543
    If this device is a mirroring device, this function returns the
544
    status of the mirror.
545

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

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

557
    The status was already read in Attach, so we just return it.
558

559
    @rtype: objects.BlockDevStatus
560

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

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

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

578
    This is a no-op for the LV device type.
579

580
    """
581
    pass
582

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

586
    This is a no-op for the LV device type.
587

588
    """
589
    pass
590

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

594
    @returns: tuple (vg, lv)
595

596
    """
597
    snap_name = self._lv_name + ".snap"
598

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

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

    
611
    _CheckResult(utils.RunCmd(["lvcreate", "-L%dm" % size, "-s",
612
                               "-n%s" % snap_name, self.dev_path]))
613

    
614
    return (self._vg_name, snap_name)
615

    
616
  def _RemoveOldInfo(self):
617
    """Try to remove old tags from the lv.
618

619
    """
620
    result = utils.RunCmd(["lvs", "-o", "tags", "--noheadings", "--nosuffix",
621
                           self.dev_path])
622
    _CheckResult(result)
623

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

    
630
  def SetInfo(self, text):
631
    """Update metadata with info text.
632

633
    """
634
    base.BlockDev.SetInfo(self, text)
635

    
636
    self._RemoveOldInfo()
637

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

    
642
    # Only up to 128 characters are allowed
643
    text = text[:128]
644

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

    
647
  def _GetGrowthAvaliabilityExclStor(self):
648
    """Return how much the disk can grow with exclusive storage.
649

650
    @rtype: float
651
    @return: available space in Mib
652

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

    
663
  def Grow(self, amount, dryrun, backingstore, excl_stor):
664
    """Grow the logical volume.
665

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

    
704
  def GetActualSpindles(self):
705
    """Return the number of spindles used.
706

707
    """
708
    assert self.attached, "BlockDevice not attached in GetActualSpindles()"
709
    return len(self.pv_names)
710

    
711

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

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

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

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

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

    
732
    filestorage.CheckFileStoragePathAcceptance(self.dev_path)
733

    
734
    self.Attach()
735

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

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

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

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

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

751
    """
752
    pass
753

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

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

759
    """
760
    pass
761

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

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

767
    """
768
    pass
769

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

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

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

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

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

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

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

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

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

816
    Check if this file already exists.
817

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

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

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

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

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

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

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

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

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

    
854
    dev_path = unique_id[1]
855

    
856
    filestorage.CheckFileStoragePathAcceptance(dev_path)
857

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

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

    
870

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

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

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

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

884
    The unique_id is a path under /dev.
885

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

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

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

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

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

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

921
    This is a noop
922

923
    """
924
    pass
925

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

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

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

935

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

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

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

    
952
    return True
953

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

957
    """
958
    pass
959

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

963
    """
964
    pass
965

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

969
    """
970
    pass
971

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

975
    """
976
    pass
977

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

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

    
984

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

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

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

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

    
1001
    self.driver, self.rbd_name = unique_id
1002

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

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

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

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

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

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

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

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

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

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

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

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

1056
    """
1057
    pass
1058

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

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

1065
    """
1066
    self.attached = False
1067

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

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

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

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

    
1085
    return True
1086

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1194
        rbd_dev = d["device"]
1195

    
1196
    return rbd_dev
1197

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

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

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

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

    
1218
    lines = output.splitlines()
1219

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

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

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

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

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

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

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

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

1255
    """
1256
    pass
1257

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

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

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

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

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

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

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

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

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

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

1296
    """
1297
    pass
1298

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

1302
    """
1303
    pass
1304

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

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

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

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

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

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

    
1338

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1401
    """
1402
    pass
1403

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

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

1410
    """
1411
    self.attached = False
1412

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

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

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

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

    
1432
    return True
1433

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

1437
    """
1438
    pass
1439

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

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

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

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

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

1459
    """
1460
    pass
1461

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

1465
    """
1466
    pass
1467

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

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

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

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

    
1487
    new_size = self.size + amount
1488

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

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

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

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

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

    
1510

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

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

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

1534
  """
1535
  driver, vol_name = unique_id
1536

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

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

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

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

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

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

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

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

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

    
1586

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

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

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

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

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

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

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

    
1617
  es_files[constants.ES_PARAMETERS_FILE] = True
1618

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

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

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

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

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

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

    
1659

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

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

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

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

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

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

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

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

    
1696
  return result
1697

    
1698

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

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

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

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

    
1719

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

    
1729

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

    
1734

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

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

    
1744

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

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

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

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

    
1765

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

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

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

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

    
1786

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

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

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