f53391859eedefa95d9c2bfc88f2d59ff0467ad7
[ganeti-local] / lib / block / bdev.py
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.block import drbd
39 from ganeti.block import base
40
41
42 class RbdShowmappedJsonError(Exception):
43   """`rbd showmmapped' JSON formatting error Exception class.
44
45   """
46   pass
47
48
49 def _CheckResult(result):
50   """Throws an error if the given result is a failed one.
51
52   @param result: result from RunCmd
53
54   """
55   if result.failed:
56     base.ThrowError("Command: %s error: %s - %s",
57                     result.cmd, result.fail_reason, result.output)
58
59
60 def _GetForbiddenFileStoragePaths():
61   """Builds a list of path prefixes which shouldn't be used for file storage.
62
63   @rtype: frozenset
64
65   """
66   paths = set([
67     "/boot",
68     "/dev",
69     "/etc",
70     "/home",
71     "/proc",
72     "/root",
73     "/sys",
74     ])
75
76   for prefix in ["", "/usr", "/usr/local"]:
77     paths.update(map(lambda s: "%s/%s" % (prefix, s),
78                      ["bin", "lib", "lib32", "lib64", "sbin"]))
79
80   return compat.UniqueFrozenset(map(os.path.normpath, paths))
81
82
83 def _ComputeWrongFileStoragePaths(paths,
84                                   _forbidden=_GetForbiddenFileStoragePaths()):
85   """Cross-checks a list of paths for prefixes considered bad.
86
87   Some paths, e.g. "/bin", should not be used for file storage.
88
89   @type paths: list
90   @param paths: List of paths to be checked
91   @rtype: list
92   @return: Sorted list of paths for which the user should be warned
93
94   """
95   def _Check(path):
96     return (not os.path.isabs(path) or
97             path in _forbidden or
98             filter(lambda p: utils.IsBelowDir(p, path), _forbidden))
99
100   return utils.NiceSort(filter(_Check, map(os.path.normpath, paths)))
101
102
103 def ComputeWrongFileStoragePaths(_filename=pathutils.FILE_STORAGE_PATHS_FILE):
104   """Returns a list of file storage paths whose prefix is considered bad.
105
106   See L{_ComputeWrongFileStoragePaths}.
107
108   """
109   return _ComputeWrongFileStoragePaths(_LoadAllowedFileStoragePaths(_filename))
110
111
112 def _CheckFileStoragePath(path, allowed):
113   """Checks if a path is in a list of allowed paths for file storage.
114
115   @type path: string
116   @param path: Path to check
117   @type allowed: list
118   @param allowed: List of allowed paths
119   @raise errors.FileStoragePathError: If the path is not allowed
120
121   """
122   if not os.path.isabs(path):
123     raise errors.FileStoragePathError("File storage path must be absolute,"
124                                       " got '%s'" % path)
125
126   for i in allowed:
127     if not os.path.isabs(i):
128       logging.info("Ignoring relative path '%s' for file storage", i)
129       continue
130
131     if utils.IsBelowDir(i, path):
132       break
133   else:
134     raise errors.FileStoragePathError("Path '%s' is not acceptable for file"
135                                       " storage" % path)
136
137
138 def _LoadAllowedFileStoragePaths(filename):
139   """Loads file containing allowed file storage paths.
140
141   @rtype: list
142   @return: List of allowed paths (can be an empty list)
143
144   """
145   try:
146     contents = utils.ReadFile(filename)
147   except EnvironmentError:
148     return []
149   else:
150     return utils.FilterEmptyLinesAndComments(contents)
151
152
153 def CheckFileStoragePath(path, _filename=pathutils.FILE_STORAGE_PATHS_FILE):
154   """Checks if a path is allowed for file storage.
155
156   @type path: string
157   @param path: Path to check
158   @raise errors.FileStoragePathError: If the path is not allowed
159
160   """
161   allowed = _LoadAllowedFileStoragePaths(_filename)
162
163   if _ComputeWrongFileStoragePaths([path]):
164     raise errors.FileStoragePathError("Path '%s' uses a forbidden prefix" %
165                                       path)
166
167   _CheckFileStoragePath(path, allowed)
168
169
170 class LogicalVolume(base.BlockDev):
171   """Logical Volume block device.
172
173   """
174   _VALID_NAME_RE = re.compile("^[a-zA-Z0-9+_.-]*$")
175   _INVALID_NAMES = compat.UniqueFrozenset([".", "..", "snapshot", "pvmove"])
176   _INVALID_SUBSTRINGS = compat.UniqueFrozenset(["_mlog", "_mimage"])
177
178   def __init__(self, unique_id, children, size, params):
179     """Attaches to a LV device.
180
181     The unique_id is a tuple (vg_name, lv_name)
182
183     """
184     super(LogicalVolume, self).__init__(unique_id, children, size, params)
185     if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
186       raise ValueError("Invalid configuration data %s" % str(unique_id))
187     self._vg_name, self._lv_name = unique_id
188     self._ValidateName(self._vg_name)
189     self._ValidateName(self._lv_name)
190     self.dev_path = utils.PathJoin("/dev", self._vg_name, self._lv_name)
191     self._degraded = True
192     self.major = self.minor = self.pe_size = self.stripe_count = None
193     self.Attach()
194
195   @staticmethod
196   def _GetStdPvSize(pvs_info):
197     """Return the the standard PV size (used with exclusive storage).
198
199     @param pvs_info: list of objects.LvmPvInfo, cannot be empty
200     @rtype: float
201     @return: size in MiB
202
203     """
204     assert len(pvs_info) > 0
205     smallest = min([pv.size for pv in pvs_info])
206     return smallest / (1 + constants.PART_MARGIN + constants.PART_RESERVED)
207
208   @staticmethod
209   def _ComputeNumPvs(size, pvs_info):
210     """Compute the number of PVs needed for an LV (with exclusive storage).
211
212     @type size: float
213     @param size: LV size in MiB
214     @param pvs_info: list of objects.LvmPvInfo, cannot be empty
215     @rtype: integer
216     @return: number of PVs needed
217     """
218     assert len(pvs_info) > 0
219     pv_size = float(LogicalVolume._GetStdPvSize(pvs_info))
220     return int(math.ceil(float(size) / pv_size))
221
222   @staticmethod
223   def _GetEmptyPvNames(pvs_info, max_pvs=None):
224     """Return a list of empty PVs, by name.
225
226     """
227     empty_pvs = filter(objects.LvmPvInfo.IsEmpty, pvs_info)
228     if max_pvs is not None:
229       empty_pvs = empty_pvs[:max_pvs]
230     return map((lambda pv: pv.name), empty_pvs)
231
232   @classmethod
233   def Create(cls, unique_id, children, size, params, excl_stor):
234     """Create a new logical volume.
235
236     """
237     if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
238       raise errors.ProgrammerError("Invalid configuration data %s" %
239                                    str(unique_id))
240     vg_name, lv_name = unique_id
241     cls._ValidateName(vg_name)
242     cls._ValidateName(lv_name)
243     pvs_info = cls.GetPVInfo([vg_name])
244     if not pvs_info:
245       if excl_stor:
246         msg = "No (empty) PVs found"
247       else:
248         msg = "Can't compute PV info for vg %s" % vg_name
249       base.ThrowError(msg)
250     pvs_info.sort(key=(lambda pv: pv.free), reverse=True)
251
252     pvlist = [pv.name for pv in pvs_info]
253     if compat.any(":" in v for v in pvlist):
254       base.ThrowError("Some of your PVs have the invalid character ':' in their"
255                       " name, this is not supported - please filter them out"
256                       " in lvm.conf using either 'filter' or 'preferred_names'")
257
258     current_pvs = len(pvlist)
259     desired_stripes = params[constants.LDP_STRIPES]
260     stripes = min(current_pvs, desired_stripes)
261
262     if excl_stor:
263       (err_msgs, _) = utils.LvmExclusiveCheckNodePvs(pvs_info)
264       if err_msgs:
265         for m in err_msgs:
266           logging.warning(m)
267       req_pvs = cls._ComputeNumPvs(size, pvs_info)
268       pvlist = cls._GetEmptyPvNames(pvs_info, req_pvs)
269       current_pvs = len(pvlist)
270       if current_pvs < req_pvs:
271         base.ThrowError("Not enough empty PVs to create a disk of %d MB:"
272                         " %d available, %d needed", size, current_pvs, req_pvs)
273       assert current_pvs == len(pvlist)
274       if stripes > current_pvs:
275         # No warning issued for this, as it's no surprise
276         stripes = current_pvs
277
278     else:
279       if stripes < desired_stripes:
280         logging.warning("Could not use %d stripes for VG %s, as only %d PVs are"
281                         " available.", desired_stripes, vg_name, current_pvs)
282       free_size = sum([pv.free for pv in pvs_info])
283       # The size constraint should have been checked from the master before
284       # calling the create function.
285       if free_size < size:
286         base.ThrowError("Not enough free space: required %s,"
287                         " available %s", size, free_size)
288
289     # If the free space is not well distributed, we won't be able to
290     # create an optimally-striped volume; in that case, we want to try
291     # with N, N-1, ..., 2, and finally 1 (non-stripped) number of
292     # stripes
293     cmd = ["lvcreate", "-L%dm" % size, "-n%s" % lv_name]
294     for stripes_arg in range(stripes, 0, -1):
295       result = utils.RunCmd(cmd + ["-i%d" % stripes_arg] + [vg_name] + pvlist)
296       if not result.failed:
297         break
298     if result.failed:
299       base.ThrowError("LV create failed (%s): %s",
300                       result.fail_reason, result.output)
301     return LogicalVolume(unique_id, children, size, params)
302
303   @staticmethod
304   def _GetVolumeInfo(lvm_cmd, fields):
305     """Returns LVM Volume infos using lvm_cmd
306
307     @param lvm_cmd: Should be one of "pvs", "vgs" or "lvs"
308     @param fields: Fields to return
309     @return: A list of dicts each with the parsed fields
310
311     """
312     if not fields:
313       raise errors.ProgrammerError("No fields specified")
314
315     sep = "|"
316     cmd = [lvm_cmd, "--noheadings", "--nosuffix", "--units=m", "--unbuffered",
317            "--separator=%s" % sep, "-o%s" % ",".join(fields)]
318
319     result = utils.RunCmd(cmd)
320     if result.failed:
321       raise errors.CommandError("Can't get the volume information: %s - %s" %
322                                 (result.fail_reason, result.output))
323
324     data = []
325     for line in result.stdout.splitlines():
326       splitted_fields = line.strip().split(sep)
327
328       if len(fields) != len(splitted_fields):
329         raise errors.CommandError("Can't parse %s output: line '%s'" %
330                                   (lvm_cmd, line))
331
332       data.append(splitted_fields)
333
334     return data
335
336   @classmethod
337   def GetPVInfo(cls, vg_names, filter_allocatable=True, include_lvs=False):
338     """Get the free space info for PVs in a volume group.
339
340     @param vg_names: list of volume group names, if empty all will be returned
341     @param filter_allocatable: whether to skip over unallocatable PVs
342     @param include_lvs: whether to include a list of LVs hosted on each PV
343
344     @rtype: list
345     @return: list of objects.LvmPvInfo objects
346
347     """
348     # We request "lv_name" field only if we care about LVs, so we don't get
349     # a long list of entries with many duplicates unless we really have to.
350     # The duplicate "pv_name" field will be ignored.
351     if include_lvs:
352       lvfield = "lv_name"
353     else:
354       lvfield = "pv_name"
355     try:
356       info = cls._GetVolumeInfo("pvs", ["pv_name", "vg_name", "pv_free",
357                                         "pv_attr", "pv_size", lvfield])
358     except errors.GenericError, err:
359       logging.error("Can't get PV information: %s", err)
360       return None
361
362     # When asked for LVs, "pvs" may return multiple entries for the same PV-LV
363     # pair. We sort entries by PV name and then LV name, so it's easy to weed
364     # out duplicates.
365     if include_lvs:
366       info.sort(key=(lambda i: (i[0], i[5])))
367     data = []
368     lastpvi = None
369     for (pv_name, vg_name, pv_free, pv_attr, pv_size, lv_name) in info:
370       # (possibly) skip over pvs which are not allocatable
371       if filter_allocatable and pv_attr[0] != "a":
372         continue
373       # (possibly) skip over pvs which are not in the right volume group(s)
374       if vg_names and vg_name not in vg_names:
375         continue
376       # Beware of duplicates (check before inserting)
377       if lastpvi and lastpvi.name == pv_name:
378         if include_lvs and lv_name:
379           if not lastpvi.lv_list or lastpvi.lv_list[-1] != lv_name:
380             lastpvi.lv_list.append(lv_name)
381       else:
382         if include_lvs and lv_name:
383           lvl = [lv_name]
384         else:
385           lvl = []
386         lastpvi = objects.LvmPvInfo(name=pv_name, vg_name=vg_name,
387                                     size=float(pv_size), free=float(pv_free),
388                                     attributes=pv_attr, lv_list=lvl)
389         data.append(lastpvi)
390
391     return data
392
393   @classmethod
394   def _GetExclusiveStorageVgFree(cls, vg_name):
395     """Return the free disk space in the given VG, in exclusive storage mode.
396
397     @type vg_name: string
398     @param vg_name: VG name
399     @rtype: float
400     @return: free space in MiB
401     """
402     pvs_info = cls.GetPVInfo([vg_name])
403     if not pvs_info:
404       return 0.0
405     pv_size = cls._GetStdPvSize(pvs_info)
406     num_pvs = len(cls._GetEmptyPvNames(pvs_info))
407     return pv_size * num_pvs
408
409   @classmethod
410   def GetVGInfo(cls, vg_names, excl_stor, filter_readonly=True):
411     """Get the free space info for specific VGs.
412
413     @param vg_names: list of volume group names, if empty all will be returned
414     @param excl_stor: whether exclusive_storage is enabled
415     @param filter_readonly: whether to skip over readonly VGs
416
417     @rtype: list
418     @return: list of tuples (free_space, total_size, name) with free_space in
419              MiB
420
421     """
422     try:
423       info = cls._GetVolumeInfo("vgs", ["vg_name", "vg_free", "vg_attr",
424                                         "vg_size"])
425     except errors.GenericError, err:
426       logging.error("Can't get VG information: %s", err)
427       return None
428
429     data = []
430     for vg_name, vg_free, vg_attr, vg_size in info:
431       # (possibly) skip over vgs which are not writable
432       if filter_readonly and vg_attr[0] == "r":
433         continue
434       # (possibly) skip over vgs which are not in the right volume group(s)
435       if vg_names and vg_name not in vg_names:
436         continue
437       # Exclusive storage needs a different concept of free space
438       if excl_stor:
439         es_free = cls._GetExclusiveStorageVgFree(vg_name)
440         assert es_free <= vg_free
441         vg_free = es_free
442       data.append((float(vg_free), float(vg_size), vg_name))
443
444     return data
445
446   @classmethod
447   def _ValidateName(cls, name):
448     """Validates that a given name is valid as VG or LV name.
449
450     The list of valid characters and restricted names is taken out of
451     the lvm(8) manpage, with the simplification that we enforce both
452     VG and LV restrictions on the names.
453
454     """
455     if (not cls._VALID_NAME_RE.match(name) or
456         name in cls._INVALID_NAMES or
457         compat.any(substring in name for substring in cls._INVALID_SUBSTRINGS)):
458       base.ThrowError("Invalid LVM name '%s'", name)
459
460   def Remove(self):
461     """Remove this logical volume.
462
463     """
464     if not self.minor and not self.Attach():
465       # the LV does not exist
466       return
467     result = utils.RunCmd(["lvremove", "-f", "%s/%s" %
468                            (self._vg_name, self._lv_name)])
469     if result.failed:
470       base.ThrowError("Can't lvremove: %s - %s",
471                       result.fail_reason, result.output)
472
473   def Rename(self, new_id):
474     """Rename this logical volume.
475
476     """
477     if not isinstance(new_id, (tuple, list)) or len(new_id) != 2:
478       raise errors.ProgrammerError("Invalid new logical id '%s'" % new_id)
479     new_vg, new_name = new_id
480     if new_vg != self._vg_name:
481       raise errors.ProgrammerError("Can't move a logical volume across"
482                                    " volume groups (from %s to to %s)" %
483                                    (self._vg_name, new_vg))
484     result = utils.RunCmd(["lvrename", new_vg, self._lv_name, new_name])
485     if result.failed:
486       base.ThrowError("Failed to rename the logical volume: %s", result.output)
487     self._lv_name = new_name
488     self.dev_path = utils.PathJoin("/dev", self._vg_name, self._lv_name)
489
490   def Attach(self):
491     """Attach to an existing LV.
492
493     This method will try to see if an existing and active LV exists
494     which matches our name. If so, its major/minor will be
495     recorded.
496
497     """
498     self.attached = False
499     result = utils.RunCmd(["lvs", "--noheadings", "--separator=,",
500                            "--units=k", "--nosuffix",
501                            "-olv_attr,lv_kernel_major,lv_kernel_minor,"
502                            "vg_extent_size,stripes", self.dev_path])
503     if result.failed:
504       logging.error("Can't find LV %s: %s, %s",
505                     self.dev_path, result.fail_reason, result.output)
506       return False
507     # the output can (and will) have multiple lines for multi-segment
508     # LVs, as the 'stripes' parameter is a segment one, so we take
509     # only the last entry, which is the one we're interested in; note
510     # that with LVM2 anyway the 'stripes' value must be constant
511     # across segments, so this is a no-op actually
512     out = result.stdout.splitlines()
513     if not out: # totally empty result? splitlines() returns at least
514                 # one line for any non-empty string
515       logging.error("Can't parse LVS output, no lines? Got '%s'", str(out))
516       return False
517     out = out[-1].strip().rstrip(",")
518     out = out.split(",")
519     if len(out) != 5:
520       logging.error("Can't parse LVS output, len(%s) != 5", str(out))
521       return False
522
523     status, major, minor, pe_size, stripes = out
524     if len(status) < 6:
525       logging.error("lvs lv_attr is not at least 6 characters (%s)", status)
526       return False
527
528     try:
529       major = int(major)
530       minor = int(minor)
531     except (TypeError, ValueError), err:
532       logging.error("lvs major/minor cannot be parsed: %s", str(err))
533
534     try:
535       pe_size = int(float(pe_size))
536     except (TypeError, ValueError), err:
537       logging.error("Can't parse vg extent size: %s", err)
538       return False
539
540     try:
541       stripes = int(stripes)
542     except (TypeError, ValueError), err:
543       logging.error("Can't parse the number of stripes: %s", err)
544       return False
545
546     self.major = major
547     self.minor = minor
548     self.pe_size = pe_size
549     self.stripe_count = stripes
550     self._degraded = status[0] == "v" # virtual volume, i.e. doesn't backing
551                                       # storage
552     self.attached = True
553     return True
554
555   def Assemble(self):
556     """Assemble the device.
557
558     We always run `lvchange -ay` on the LV to ensure it's active before
559     use, as there were cases when xenvg was not active after boot
560     (also possibly after disk issues).
561
562     """
563     result = utils.RunCmd(["lvchange", "-ay", self.dev_path])
564     if result.failed:
565       base.ThrowError("Can't activate lv %s: %s", self.dev_path, result.output)
566
567   def Shutdown(self):
568     """Shutdown the device.
569
570     This is a no-op for the LV device type, as we don't deactivate the
571     volumes on shutdown.
572
573     """
574     pass
575
576   def GetSyncStatus(self):
577     """Returns the sync status of the device.
578
579     If this device is a mirroring device, this function returns the
580     status of the mirror.
581
582     For logical volumes, sync_percent and estimated_time are always
583     None (no recovery in progress, as we don't handle the mirrored LV
584     case). The is_degraded parameter is the inverse of the ldisk
585     parameter.
586
587     For the ldisk parameter, we check if the logical volume has the
588     'virtual' type, which means it's not backed by existing storage
589     anymore (read from it return I/O error). This happens after a
590     physical disk failure and subsequent 'vgreduce --removemissing' on
591     the volume group.
592
593     The status was already read in Attach, so we just return it.
594
595     @rtype: objects.BlockDevStatus
596
597     """
598     if self._degraded:
599       ldisk_status = constants.LDS_FAULTY
600     else:
601       ldisk_status = constants.LDS_OKAY
602
603     return objects.BlockDevStatus(dev_path=self.dev_path,
604                                   major=self.major,
605                                   minor=self.minor,
606                                   sync_percent=None,
607                                   estimated_time=None,
608                                   is_degraded=self._degraded,
609                                   ldisk_status=ldisk_status)
610
611   def Open(self, force=False):
612     """Make the device ready for I/O.
613
614     This is a no-op for the LV device type.
615
616     """
617     pass
618
619   def Close(self):
620     """Notifies that the device will no longer be used for I/O.
621
622     This is a no-op for the LV device type.
623
624     """
625     pass
626
627   def Snapshot(self, size):
628     """Create a snapshot copy of an lvm block device.
629
630     @returns: tuple (vg, lv)
631
632     """
633     snap_name = self._lv_name + ".snap"
634
635     # remove existing snapshot if found
636     snap = LogicalVolume((self._vg_name, snap_name), None, size, self.params)
637     base.IgnoreError(snap.Remove)
638
639     vg_info = self.GetVGInfo([self._vg_name], False)
640     if not vg_info:
641       base.ThrowError("Can't compute VG info for vg %s", self._vg_name)
642     free_size, _, _ = vg_info[0]
643     if free_size < size:
644       base.ThrowError("Not enough free space: required %s,"
645                       " available %s", size, free_size)
646
647     _CheckResult(utils.RunCmd(["lvcreate", "-L%dm" % size, "-s",
648                                "-n%s" % snap_name, self.dev_path]))
649
650     return (self._vg_name, snap_name)
651
652   def _RemoveOldInfo(self):
653     """Try to remove old tags from the lv.
654
655     """
656     result = utils.RunCmd(["lvs", "-o", "tags", "--noheadings", "--nosuffix",
657                            self.dev_path])
658     _CheckResult(result)
659
660     raw_tags = result.stdout.strip()
661     if raw_tags:
662       for tag in raw_tags.split(","):
663         _CheckResult(utils.RunCmd(["lvchange", "--deltag",
664                                    tag.strip(), self.dev_path]))
665
666   def SetInfo(self, text):
667     """Update metadata with info text.
668
669     """
670     base.BlockDev.SetInfo(self, text)
671
672     self._RemoveOldInfo()
673
674     # Replace invalid characters
675     text = re.sub("^[^A-Za-z0-9_+.]", "_", text)
676     text = re.sub("[^-A-Za-z0-9_+.]", "_", text)
677
678     # Only up to 128 characters are allowed
679     text = text[:128]
680
681     _CheckResult(utils.RunCmd(["lvchange", "--addtag", text, self.dev_path]))
682
683   def Grow(self, amount, dryrun, backingstore):
684     """Grow the logical volume.
685
686     """
687     if not backingstore:
688       return
689     if self.pe_size is None or self.stripe_count is None:
690       if not self.Attach():
691         base.ThrowError("Can't attach to LV during Grow()")
692     full_stripe_size = self.pe_size * self.stripe_count
693     # pe_size is in KB
694     amount *= 1024
695     rest = amount % full_stripe_size
696     if rest != 0:
697       amount += full_stripe_size - rest
698     cmd = ["lvextend", "-L", "+%dk" % amount]
699     if dryrun:
700       cmd.append("--test")
701     # we try multiple algorithms since the 'best' ones might not have
702     # space available in the right place, but later ones might (since
703     # they have less constraints); also note that only recent LVM
704     # supports 'cling'
705     for alloc_policy in "contiguous", "cling", "normal":
706       result = utils.RunCmd(cmd + ["--alloc", alloc_policy, self.dev_path])
707       if not result.failed:
708         return
709     base.ThrowError("Can't grow LV %s: %s", self.dev_path, result.output)
710
711
712 class FileStorage(base.BlockDev):
713   """File device.
714
715   This class represents the a file storage backend device.
716
717   The unique_id for the file device is a (file_driver, file_path) tuple.
718
719   """
720   def __init__(self, unique_id, children, size, params):
721     """Initalizes a file device backend.
722
723     """
724     if children:
725       raise errors.BlockDeviceError("Invalid setup for file device")
726     super(FileStorage, self).__init__(unique_id, children, size, params)
727     if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
728       raise ValueError("Invalid configuration data %s" % str(unique_id))
729     self.driver = unique_id[0]
730     self.dev_path = unique_id[1]
731
732     CheckFileStoragePath(self.dev_path)
733
734     self.Attach()
735
736   def Assemble(self):
737     """Assemble the device.
738
739     Checks whether the file device exists, raises BlockDeviceError otherwise.
740
741     """
742     if not os.path.exists(self.dev_path):
743       base.ThrowError("File device '%s' does not exist" % self.dev_path)
744
745   def Shutdown(self):
746     """Shutdown the device.
747
748     This is a no-op for the file type, as we don't deactivate
749     the file on shutdown.
750
751     """
752     pass
753
754   def Open(self, force=False):
755     """Make the device ready for I/O.
756
757     This is a no-op for the file type.
758
759     """
760     pass
761
762   def Close(self):
763     """Notifies that the device will no longer be used for I/O.
764
765     This is a no-op for the file type.
766
767     """
768     pass
769
770   def Remove(self):
771     """Remove the file backing the block device.
772
773     @rtype: boolean
774     @return: True if the removal was successful
775
776     """
777     try:
778       os.remove(self.dev_path)
779     except OSError, err:
780       if err.errno != errno.ENOENT:
781         base.ThrowError("Can't remove file '%s': %s", self.dev_path, err)
782
783   def Rename(self, new_id):
784     """Renames the file.
785
786     """
787     # TODO: implement rename for file-based storage
788     base.ThrowError("Rename is not supported for file-based storage")
789
790   def Grow(self, amount, dryrun, backingstore):
791     """Grow the file
792
793     @param amount: the amount (in mebibytes) to grow with
794
795     """
796     if not backingstore:
797       return
798     # Check that the file exists
799     self.Assemble()
800     current_size = self.GetActualSize()
801     new_size = current_size + amount * 1024 * 1024
802     assert new_size > current_size, "Cannot Grow with a negative amount"
803     # We can't really simulate the growth
804     if dryrun:
805       return
806     try:
807       f = open(self.dev_path, "a+")
808       f.truncate(new_size)
809       f.close()
810     except EnvironmentError, err:
811       base.ThrowError("Error in file growth: %", str(err))
812
813   def Attach(self):
814     """Attach to an existing file.
815
816     Check if this file already exists.
817
818     @rtype: boolean
819     @return: True if file exists
820
821     """
822     self.attached = os.path.exists(self.dev_path)
823     return self.attached
824
825   def GetActualSize(self):
826     """Return the actual disk size.
827
828     @note: the device needs to be active when this is called
829
830     """
831     assert self.attached, "BlockDevice not attached in GetActualSize()"
832     try:
833       st = os.stat(self.dev_path)
834       return st.st_size
835     except OSError, err:
836       base.ThrowError("Can't stat %s: %s", self.dev_path, err)
837
838   @classmethod
839   def Create(cls, unique_id, children, size, params, excl_stor):
840     """Create a new file.
841
842     @param size: the size of file in MiB
843
844     @rtype: L{bdev.FileStorage}
845     @return: an instance of FileStorage
846
847     """
848     if excl_stor:
849       raise errors.ProgrammerError("FileStorage device requested with"
850                                    " exclusive_storage")
851     if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
852       raise ValueError("Invalid configuration data %s" % str(unique_id))
853
854     dev_path = unique_id[1]
855
856     CheckFileStoragePath(dev_path)
857
858     try:
859       fd = os.open(dev_path, os.O_RDWR | os.O_CREAT | os.O_EXCL)
860       f = os.fdopen(fd, "w")
861       f.truncate(size * 1024 * 1024)
862       f.close()
863     except EnvironmentError, err:
864       if err.errno == errno.EEXIST:
865         base.ThrowError("File already existing: %s", dev_path)
866       base.ThrowError("Error in file creation: %", str(err))
867
868     return FileStorage(unique_id, children, size, params)
869
870
871 class PersistentBlockDevice(base.BlockDev):
872   """A block device with persistent node
873
874   May be either directly attached, or exposed through DM (e.g. dm-multipath).
875   udev helpers are probably required to give persistent, human-friendly
876   names.
877
878   For the time being, pathnames are required to lie under /dev.
879
880   """
881   def __init__(self, unique_id, children, size, params):
882     """Attaches to a static block device.
883
884     The unique_id is a path under /dev.
885
886     """
887     super(PersistentBlockDevice, self).__init__(unique_id, children, size,
888                                                 params)
889     if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
890       raise ValueError("Invalid configuration data %s" % str(unique_id))
891     self.dev_path = unique_id[1]
892     if not os.path.realpath(self.dev_path).startswith("/dev/"):
893       raise ValueError("Full path '%s' lies outside /dev" %
894                               os.path.realpath(self.dev_path))
895     # TODO: this is just a safety guard checking that we only deal with devices
896     # we know how to handle. In the future this will be integrated with
897     # external storage backends and possible values will probably be collected
898     # from the cluster configuration.
899     if unique_id[0] != constants.BLOCKDEV_DRIVER_MANUAL:
900       raise ValueError("Got persistent block device of invalid type: %s" %
901                        unique_id[0])
902
903     self.major = self.minor = None
904     self.Attach()
905
906   @classmethod
907   def Create(cls, unique_id, children, size, params, excl_stor):
908     """Create a new device
909
910     This is a noop, we only return a PersistentBlockDevice instance
911
912     """
913     if excl_stor:
914       raise errors.ProgrammerError("Persistent block device requested with"
915                                    " exclusive_storage")
916     return PersistentBlockDevice(unique_id, children, 0, params)
917
918   def Remove(self):
919     """Remove a device
920
921     This is a noop
922
923     """
924     pass
925
926   def Rename(self, new_id):
927     """Rename this device.
928
929     """
930     base.ThrowError("Rename is not supported for PersistentBlockDev storage")
931
932   def Attach(self):
933     """Attach to an existing block device.
934
935
936     """
937     self.attached = False
938     try:
939       st = os.stat(self.dev_path)
940     except OSError, err:
941       logging.error("Error stat()'ing %s: %s", self.dev_path, str(err))
942       return False
943
944     if not stat.S_ISBLK(st.st_mode):
945       logging.error("%s is not a block device", self.dev_path)
946       return False
947
948     self.major = os.major(st.st_rdev)
949     self.minor = os.minor(st.st_rdev)
950     self.attached = True
951
952     return True
953
954   def Assemble(self):
955     """Assemble the device.
956
957     """
958     pass
959
960   def Shutdown(self):
961     """Shutdown the device.
962
963     """
964     pass
965
966   def Open(self, force=False):
967     """Make the device ready for I/O.
968
969     """
970     pass
971
972   def Close(self):
973     """Notifies that the device will no longer be used for I/O.
974
975     """
976     pass
977
978   def Grow(self, amount, dryrun, backingstore):
979     """Grow the logical volume.
980
981     """
982     base.ThrowError("Grow is not supported for PersistentBlockDev storage")
983
984
985 class RADOSBlockDevice(base.BlockDev):
986   """A RADOS Block Device (rbd).
987
988   This class implements the RADOS Block Device for the backend. You need
989   the rbd kernel driver, the RADOS Tools and a working RADOS cluster for
990   this to be functional.
991
992   """
993   def __init__(self, unique_id, children, size, params):
994     """Attaches to an rbd device.
995
996     """
997     super(RADOSBlockDevice, self).__init__(unique_id, children, size, params)
998     if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
999       raise ValueError("Invalid configuration data %s" % str(unique_id))
1000
1001     self.driver, self.rbd_name = unique_id
1002
1003     self.major = self.minor = None
1004     self.Attach()
1005
1006   @classmethod
1007   def Create(cls, unique_id, children, size, params, excl_stor):
1008     """Create a new rbd device.
1009
1010     Provision a new rbd volume inside a RADOS pool.
1011
1012     """
1013     if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1014       raise errors.ProgrammerError("Invalid configuration data %s" %
1015                                    str(unique_id))
1016     if excl_stor:
1017       raise errors.ProgrammerError("RBD device requested with"
1018                                    " exclusive_storage")
1019     rbd_pool = params[constants.LDP_POOL]
1020     rbd_name = unique_id[1]
1021
1022     # Provision a new rbd volume (Image) inside the RADOS cluster.
1023     cmd = [constants.RBD_CMD, "create", "-p", rbd_pool,
1024            rbd_name, "--size", "%s" % size]
1025     result = utils.RunCmd(cmd)
1026     if result.failed:
1027       base.ThrowError("rbd creation failed (%s): %s",
1028                       result.fail_reason, result.output)
1029
1030     return RADOSBlockDevice(unique_id, children, size, params)
1031
1032   def Remove(self):
1033     """Remove the rbd device.
1034
1035     """
1036     rbd_pool = self.params[constants.LDP_POOL]
1037     rbd_name = self.unique_id[1]
1038
1039     if not self.minor and not self.Attach():
1040       # The rbd device doesn't exist.
1041       return
1042
1043     # First shutdown the device (remove mappings).
1044     self.Shutdown()
1045
1046     # Remove the actual Volume (Image) from the RADOS cluster.
1047     cmd = [constants.RBD_CMD, "rm", "-p", rbd_pool, rbd_name]
1048     result = utils.RunCmd(cmd)
1049     if result.failed:
1050       base.ThrowError("Can't remove Volume from cluster with rbd rm: %s - %s",
1051                       result.fail_reason, result.output)
1052
1053   def Rename(self, new_id):
1054     """Rename this device.
1055
1056     """
1057     pass
1058
1059   def Attach(self):
1060     """Attach to an existing rbd device.
1061
1062     This method maps the rbd volume that matches our name with
1063     an rbd device and then attaches to this device.
1064
1065     """
1066     self.attached = False
1067
1068     # Map the rbd volume to a block device under /dev
1069     self.dev_path = self._MapVolumeToBlockdev(self.unique_id)
1070
1071     try:
1072       st = os.stat(self.dev_path)
1073     except OSError, err:
1074       logging.error("Error stat()'ing %s: %s", self.dev_path, str(err))
1075       return False
1076
1077     if not stat.S_ISBLK(st.st_mode):
1078       logging.error("%s is not a block device", self.dev_path)
1079       return False
1080
1081     self.major = os.major(st.st_rdev)
1082     self.minor = os.minor(st.st_rdev)
1083     self.attached = True
1084
1085     return True
1086
1087   def _MapVolumeToBlockdev(self, unique_id):
1088     """Maps existing rbd volumes to block devices.
1089
1090     This method should be idempotent if the mapping already exists.
1091
1092     @rtype: string
1093     @return: the block device path that corresponds to the volume
1094
1095     """
1096     pool = self.params[constants.LDP_POOL]
1097     name = unique_id[1]
1098
1099     # Check if the mapping already exists.
1100     rbd_dev = self._VolumeToBlockdev(pool, name)
1101     if rbd_dev:
1102       # The mapping exists. Return it.
1103       return rbd_dev
1104
1105     # The mapping doesn't exist. Create it.
1106     map_cmd = [constants.RBD_CMD, "map", "-p", pool, name]
1107     result = utils.RunCmd(map_cmd)
1108     if result.failed:
1109       base.ThrowError("rbd map failed (%s): %s",
1110                       result.fail_reason, result.output)
1111
1112     # Find the corresponding rbd device.
1113     rbd_dev = self._VolumeToBlockdev(pool, name)
1114     if not rbd_dev:
1115       base.ThrowError("rbd map succeeded, but could not find the rbd block"
1116                       " device in output of showmapped, for volume: %s", name)
1117
1118     # The device was successfully mapped. Return it.
1119     return rbd_dev
1120
1121   @classmethod
1122   def _VolumeToBlockdev(cls, pool, volume_name):
1123     """Do the 'volume name'-to-'rbd block device' resolving.
1124
1125     @type pool: string
1126     @param pool: RADOS pool to use
1127     @type volume_name: string
1128     @param volume_name: the name of the volume whose device we search for
1129     @rtype: string or None
1130     @return: block device path if the volume is mapped, else None
1131
1132     """
1133     try:
1134       # Newer versions of the rbd tool support json output formatting. Use it
1135       # if available.
1136       showmap_cmd = [
1137         constants.RBD_CMD,
1138         "showmapped",
1139         "-p",
1140         pool,
1141         "--format",
1142         "json"
1143         ]
1144       result = utils.RunCmd(showmap_cmd)
1145       if result.failed:
1146         logging.error("rbd JSON output formatting returned error (%s): %s,"
1147                       "falling back to plain output parsing",
1148                       result.fail_reason, result.output)
1149         raise RbdShowmappedJsonError
1150
1151       return cls._ParseRbdShowmappedJson(result.output, volume_name)
1152     except RbdShowmappedJsonError:
1153       # For older versions of rbd, we have to parse the plain / text output
1154       # manually.
1155       showmap_cmd = [constants.RBD_CMD, "showmapped", "-p", pool]
1156       result = utils.RunCmd(showmap_cmd)
1157       if result.failed:
1158         base.ThrowError("rbd showmapped failed (%s): %s",
1159                         result.fail_reason, result.output)
1160
1161       return cls._ParseRbdShowmappedPlain(result.output, volume_name)
1162
1163   @staticmethod
1164   def _ParseRbdShowmappedJson(output, volume_name):
1165     """Parse the json output of `rbd showmapped'.
1166
1167     This method parses the json output of `rbd showmapped' and returns the rbd
1168     block device path (e.g. /dev/rbd0) that matches the given rbd volume.
1169
1170     @type output: string
1171     @param output: the json output of `rbd showmapped'
1172     @type volume_name: string
1173     @param volume_name: the name of the volume whose device we search for
1174     @rtype: string or None
1175     @return: block device path if the volume is mapped, else None
1176
1177     """
1178     try:
1179       devices = serializer.LoadJson(output)
1180     except ValueError, err:
1181       base.ThrowError("Unable to parse JSON data: %s" % err)
1182
1183     rbd_dev = None
1184     for d in devices.values(): # pylint: disable=E1103
1185       try:
1186         name = d["name"]
1187       except KeyError:
1188         base.ThrowError("'name' key missing from json object %s", devices)
1189
1190       if name == volume_name:
1191         if rbd_dev is not None:
1192           base.ThrowError("rbd volume %s is mapped more than once", volume_name)
1193
1194         rbd_dev = d["device"]
1195
1196     return rbd_dev
1197
1198   @staticmethod
1199   def _ParseRbdShowmappedPlain(output, volume_name):
1200     """Parse the (plain / text) output of `rbd showmapped'.
1201
1202     This method parses the output of `rbd showmapped' and returns
1203     the rbd block device path (e.g. /dev/rbd0) that matches the
1204     given rbd volume.
1205
1206     @type output: string
1207     @param output: the plain text output of `rbd showmapped'
1208     @type volume_name: string
1209     @param volume_name: the name of the volume whose device we search for
1210     @rtype: string or None
1211     @return: block device path if the volume is mapped, else None
1212
1213     """
1214     allfields = 5
1215     volumefield = 2
1216     devicefield = 4
1217
1218     lines = output.splitlines()
1219
1220     # Try parsing the new output format (ceph >= 0.55).
1221     splitted_lines = map(lambda l: l.split(), lines)
1222
1223     # Check for empty output.
1224     if not splitted_lines:
1225       return None
1226
1227     # Check showmapped output, to determine number of fields.
1228     field_cnt = len(splitted_lines[0])
1229     if field_cnt != allfields:
1230       # Parsing the new format failed. Fallback to parsing the old output
1231       # format (< 0.55).
1232       splitted_lines = map(lambda l: l.split("\t"), lines)
1233       if field_cnt != allfields:
1234         base.ThrowError("Cannot parse rbd showmapped output expected %s fields,"
1235                         " found %s", allfields, field_cnt)
1236
1237     matched_lines = \
1238       filter(lambda l: len(l) == allfields and l[volumefield] == volume_name,
1239              splitted_lines)
1240
1241     if len(matched_lines) > 1:
1242       base.ThrowError("rbd volume %s mapped more than once", volume_name)
1243
1244     if matched_lines:
1245       # rbd block device found. Return it.
1246       rbd_dev = matched_lines[0][devicefield]
1247       return rbd_dev
1248
1249     # The given volume is not mapped.
1250     return None
1251
1252   def Assemble(self):
1253     """Assemble the device.
1254
1255     """
1256     pass
1257
1258   def Shutdown(self):
1259     """Shutdown the device.
1260
1261     """
1262     if not self.minor and not self.Attach():
1263       # The rbd device doesn't exist.
1264       return
1265
1266     # Unmap the block device from the Volume.
1267     self._UnmapVolumeFromBlockdev(self.unique_id)
1268
1269     self.minor = None
1270     self.dev_path = None
1271
1272   def _UnmapVolumeFromBlockdev(self, unique_id):
1273     """Unmaps the rbd device from the Volume it is mapped.
1274
1275     Unmaps the rbd device from the Volume it was previously mapped to.
1276     This method should be idempotent if the Volume isn't mapped.
1277
1278     """
1279     pool = self.params[constants.LDP_POOL]
1280     name = unique_id[1]
1281
1282     # Check if the mapping already exists.
1283     rbd_dev = self._VolumeToBlockdev(pool, name)
1284
1285     if rbd_dev:
1286       # The mapping exists. Unmap the rbd device.
1287       unmap_cmd = [constants.RBD_CMD, "unmap", "%s" % rbd_dev]
1288       result = utils.RunCmd(unmap_cmd)
1289       if result.failed:
1290         base.ThrowError("rbd unmap failed (%s): %s",
1291                         result.fail_reason, result.output)
1292
1293   def Open(self, force=False):
1294     """Make the device ready for I/O.
1295
1296     """
1297     pass
1298
1299   def Close(self):
1300     """Notifies that the device will no longer be used for I/O.
1301
1302     """
1303     pass
1304
1305   def Grow(self, amount, dryrun, backingstore):
1306     """Grow the Volume.
1307
1308     @type amount: integer
1309     @param amount: the amount (in mebibytes) to grow with
1310     @type dryrun: boolean
1311     @param dryrun: whether to execute the operation in simulation mode
1312         only, without actually increasing the size
1313
1314     """
1315     if not backingstore:
1316       return
1317     if not self.Attach():
1318       base.ThrowError("Can't attach to rbd device during Grow()")
1319
1320     if dryrun:
1321       # the rbd tool does not support dry runs of resize operations.
1322       # Since rbd volumes are thinly provisioned, we assume
1323       # there is always enough free space for the operation.
1324       return
1325
1326     rbd_pool = self.params[constants.LDP_POOL]
1327     rbd_name = self.unique_id[1]
1328     new_size = self.size + amount
1329
1330     # Resize the rbd volume (Image) inside the RADOS cluster.
1331     cmd = [constants.RBD_CMD, "resize", "-p", rbd_pool,
1332            rbd_name, "--size", "%s" % new_size]
1333     result = utils.RunCmd(cmd)
1334     if result.failed:
1335       base.ThrowError("rbd resize failed (%s): %s",
1336                       result.fail_reason, result.output)
1337
1338
1339 class ExtStorageDevice(base.BlockDev):
1340   """A block device provided by an ExtStorage Provider.
1341
1342   This class implements the External Storage Interface, which means
1343   handling of the externally provided block devices.
1344
1345   """
1346   def __init__(self, unique_id, children, size, params):
1347     """Attaches to an extstorage block device.
1348
1349     """
1350     super(ExtStorageDevice, self).__init__(unique_id, children, size, params)
1351     if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1352       raise ValueError("Invalid configuration data %s" % str(unique_id))
1353
1354     self.driver, self.vol_name = unique_id
1355     self.ext_params = params
1356
1357     self.major = self.minor = None
1358     self.Attach()
1359
1360   @classmethod
1361   def Create(cls, unique_id, children, size, params, excl_stor):
1362     """Create a new extstorage device.
1363
1364     Provision a new volume using an extstorage provider, which will
1365     then be mapped to a block device.
1366
1367     """
1368     if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1369       raise errors.ProgrammerError("Invalid configuration data %s" %
1370                                    str(unique_id))
1371     if excl_stor:
1372       raise errors.ProgrammerError("extstorage device requested with"
1373                                    " exclusive_storage")
1374
1375     # Call the External Storage's create script,
1376     # to provision a new Volume inside the External Storage
1377     _ExtStorageAction(constants.ES_ACTION_CREATE, unique_id,
1378                       params, str(size))
1379
1380     return ExtStorageDevice(unique_id, children, size, params)
1381
1382   def Remove(self):
1383     """Remove the extstorage device.
1384
1385     """
1386     if not self.minor and not self.Attach():
1387       # The extstorage device doesn't exist.
1388       return
1389
1390     # First shutdown the device (remove mappings).
1391     self.Shutdown()
1392
1393     # Call the External Storage's remove script,
1394     # to remove the Volume from the External Storage
1395     _ExtStorageAction(constants.ES_ACTION_REMOVE, self.unique_id,
1396                       self.ext_params)
1397
1398   def Rename(self, new_id):
1399     """Rename this device.
1400
1401     """
1402     pass
1403
1404   def Attach(self):
1405     """Attach to an existing extstorage device.
1406
1407     This method maps the extstorage volume that matches our name with
1408     a corresponding block device and then attaches to this device.
1409
1410     """
1411     self.attached = False
1412
1413     # Call the External Storage's attach script,
1414     # to attach an existing Volume to a block device under /dev
1415     self.dev_path = _ExtStorageAction(constants.ES_ACTION_ATTACH,
1416                                       self.unique_id, self.ext_params)
1417
1418     try:
1419       st = os.stat(self.dev_path)
1420     except OSError, err:
1421       logging.error("Error stat()'ing %s: %s", self.dev_path, str(err))
1422       return False
1423
1424     if not stat.S_ISBLK(st.st_mode):
1425       logging.error("%s is not a block device", self.dev_path)
1426       return False
1427
1428     self.major = os.major(st.st_rdev)
1429     self.minor = os.minor(st.st_rdev)
1430     self.attached = True
1431
1432     return True
1433
1434   def Assemble(self):
1435     """Assemble the device.
1436
1437     """
1438     pass
1439
1440   def Shutdown(self):
1441     """Shutdown the device.
1442
1443     """
1444     if not self.minor and not self.Attach():
1445       # The extstorage device doesn't exist.
1446       return
1447
1448     # Call the External Storage's detach script,
1449     # to detach an existing Volume from it's block device under /dev
1450     _ExtStorageAction(constants.ES_ACTION_DETACH, self.unique_id,
1451                       self.ext_params)
1452
1453     self.minor = None
1454     self.dev_path = None
1455
1456   def Open(self, force=False):
1457     """Make the device ready for I/O.
1458
1459     """
1460     pass
1461
1462   def Close(self):
1463     """Notifies that the device will no longer be used for I/O.
1464
1465     """
1466     pass
1467
1468   def Grow(self, amount, dryrun, backingstore):
1469     """Grow the Volume.
1470
1471     @type amount: integer
1472     @param amount: the amount (in mebibytes) to grow with
1473     @type dryrun: boolean
1474     @param dryrun: whether to execute the operation in simulation mode
1475         only, without actually increasing the size
1476
1477     """
1478     if not backingstore:
1479       return
1480     if not self.Attach():
1481       base.ThrowError("Can't attach to extstorage device during Grow()")
1482
1483     if dryrun:
1484       # we do not support dry runs of resize operations for now.
1485       return
1486
1487     new_size = self.size + amount
1488
1489     # Call the External Storage's grow script,
1490     # to grow an existing Volume inside the External Storage
1491     _ExtStorageAction(constants.ES_ACTION_GROW, self.unique_id,
1492                       self.ext_params, str(self.size), grow=str(new_size))
1493
1494   def SetInfo(self, text):
1495     """Update metadata with info text.
1496
1497     """
1498     # Replace invalid characters
1499     text = re.sub("^[^A-Za-z0-9_+.]", "_", text)
1500     text = re.sub("[^-A-Za-z0-9_+.]", "_", text)
1501
1502     # Only up to 128 characters are allowed
1503     text = text[:128]
1504
1505     # Call the External Storage's setinfo script,
1506     # to set metadata for an existing Volume inside the External Storage
1507     _ExtStorageAction(constants.ES_ACTION_SETINFO, self.unique_id,
1508                       self.ext_params, metadata=text)
1509
1510
1511 def _ExtStorageAction(action, unique_id, ext_params,
1512                       size=None, grow=None, metadata=None):
1513   """Take an External Storage action.
1514
1515   Take an External Storage action concerning or affecting
1516   a specific Volume inside the External Storage.
1517
1518   @type action: string
1519   @param action: which action to perform. One of:
1520                  create / remove / grow / attach / detach
1521   @type unique_id: tuple (driver, vol_name)
1522   @param unique_id: a tuple containing the type of ExtStorage (driver)
1523                     and the Volume name
1524   @type ext_params: dict
1525   @param ext_params: ExtStorage parameters
1526   @type size: integer
1527   @param size: the size of the Volume in mebibytes
1528   @type grow: integer
1529   @param grow: the new size in mebibytes (after grow)
1530   @type metadata: string
1531   @param metadata: metadata info of the Volume, for use by the provider
1532   @rtype: None or a block device path (during attach)
1533
1534   """
1535   driver, vol_name = unique_id
1536
1537   # Create an External Storage instance of type `driver'
1538   status, inst_es = ExtStorageFromDisk(driver)
1539   if not status:
1540     base.ThrowError("%s" % inst_es)
1541
1542   # Create the basic environment for the driver's scripts
1543   create_env = _ExtStorageEnvironment(unique_id, ext_params, size,
1544                                       grow, metadata)
1545
1546   # Do not use log file for action `attach' as we need
1547   # to get the output from RunResult
1548   # TODO: find a way to have a log file for attach too
1549   logfile = None
1550   if action is not constants.ES_ACTION_ATTACH:
1551     logfile = _VolumeLogName(action, driver, vol_name)
1552
1553   # Make sure the given action results in a valid script
1554   if action not in constants.ES_SCRIPTS:
1555     base.ThrowError("Action '%s' doesn't result in a valid ExtStorage script" %
1556                     action)
1557
1558   # Find out which external script to run according the given action
1559   script_name = action + "_script"
1560   script = getattr(inst_es, script_name)
1561
1562   # Run the external script
1563   result = utils.RunCmd([script], env=create_env,
1564                         cwd=inst_es.path, output=logfile,)
1565   if result.failed:
1566     logging.error("External storage's %s command '%s' returned"
1567                   " error: %s, logfile: %s, output: %s",
1568                   action, result.cmd, result.fail_reason,
1569                   logfile, result.output)
1570
1571     # If logfile is 'None' (during attach), it breaks TailFile
1572     # TODO: have a log file for attach too
1573     if action is not constants.ES_ACTION_ATTACH:
1574       lines = [utils.SafeEncode(val)
1575                for val in utils.TailFile(logfile, lines=20)]
1576     else:
1577       lines = result.output[-20:]
1578
1579     base.ThrowError("External storage's %s script failed (%s), last"
1580                     " lines of output:\n%s",
1581                     action, result.fail_reason, "\n".join(lines))
1582
1583   if action == constants.ES_ACTION_ATTACH:
1584     return result.stdout
1585
1586
1587 def ExtStorageFromDisk(name, base_dir=None):
1588   """Create an ExtStorage instance from disk.
1589
1590   This function will return an ExtStorage instance
1591   if the given name is a valid ExtStorage name.
1592
1593   @type base_dir: string
1594   @keyword base_dir: Base directory containing ExtStorage installations.
1595                      Defaults to a search in all the ES_SEARCH_PATH dirs.
1596   @rtype: tuple
1597   @return: True and the ExtStorage instance if we find a valid one, or
1598       False and the diagnose message on error
1599
1600   """
1601   if base_dir is None:
1602     es_base_dir = pathutils.ES_SEARCH_PATH
1603   else:
1604     es_base_dir = [base_dir]
1605
1606   es_dir = utils.FindFile(name, es_base_dir, os.path.isdir)
1607
1608   if es_dir is None:
1609     return False, ("Directory for External Storage Provider %s not"
1610                    " found in search path" % name)
1611
1612   # ES Files dictionary, we will populate it with the absolute path
1613   # names; if the value is True, then it is a required file, otherwise
1614   # an optional one
1615   es_files = dict.fromkeys(constants.ES_SCRIPTS, True)
1616
1617   es_files[constants.ES_PARAMETERS_FILE] = True
1618
1619   for (filename, _) in es_files.items():
1620     es_files[filename] = utils.PathJoin(es_dir, filename)
1621
1622     try:
1623       st = os.stat(es_files[filename])
1624     except EnvironmentError, err:
1625       return False, ("File '%s' under path '%s' is missing (%s)" %
1626                      (filename, es_dir, utils.ErrnoOrStr(err)))
1627
1628     if not stat.S_ISREG(stat.S_IFMT(st.st_mode)):
1629       return False, ("File '%s' under path '%s' is not a regular file" %
1630                      (filename, es_dir))
1631
1632     if filename in constants.ES_SCRIPTS:
1633       if stat.S_IMODE(st.st_mode) & stat.S_IXUSR != stat.S_IXUSR:
1634         return False, ("File '%s' under path '%s' is not executable" %
1635                        (filename, es_dir))
1636
1637   parameters = []
1638   if constants.ES_PARAMETERS_FILE in es_files:
1639     parameters_file = es_files[constants.ES_PARAMETERS_FILE]
1640     try:
1641       parameters = utils.ReadFile(parameters_file).splitlines()
1642     except EnvironmentError, err:
1643       return False, ("Error while reading the EXT parameters file at %s: %s" %
1644                      (parameters_file, utils.ErrnoOrStr(err)))
1645     parameters = [v.split(None, 1) for v in parameters]
1646
1647   es_obj = \
1648     objects.ExtStorage(name=name, path=es_dir,
1649                        create_script=es_files[constants.ES_SCRIPT_CREATE],
1650                        remove_script=es_files[constants.ES_SCRIPT_REMOVE],
1651                        grow_script=es_files[constants.ES_SCRIPT_GROW],
1652                        attach_script=es_files[constants.ES_SCRIPT_ATTACH],
1653                        detach_script=es_files[constants.ES_SCRIPT_DETACH],
1654                        setinfo_script=es_files[constants.ES_SCRIPT_SETINFO],
1655                        verify_script=es_files[constants.ES_SCRIPT_VERIFY],
1656                        supported_parameters=parameters)
1657   return True, es_obj
1658
1659
1660 def _ExtStorageEnvironment(unique_id, ext_params,
1661                            size=None, grow=None, metadata=None):
1662   """Calculate the environment for an External Storage script.
1663
1664   @type unique_id: tuple (driver, vol_name)
1665   @param unique_id: ExtStorage pool and name of the Volume
1666   @type ext_params: dict
1667   @param ext_params: the EXT parameters
1668   @type size: string
1669   @param size: size of the Volume (in mebibytes)
1670   @type grow: string
1671   @param grow: new size of Volume after grow (in mebibytes)
1672   @type metadata: string
1673   @param metadata: metadata info of the Volume
1674   @rtype: dict
1675   @return: dict of environment variables
1676
1677   """
1678   vol_name = unique_id[1]
1679
1680   result = {}
1681   result["VOL_NAME"] = vol_name
1682
1683   # EXT params
1684   for pname, pvalue in ext_params.items():
1685     result["EXTP_%s" % pname.upper()] = str(pvalue)
1686
1687   if size is not None:
1688     result["VOL_SIZE"] = size
1689
1690   if grow is not None:
1691     result["VOL_NEW_SIZE"] = grow
1692
1693   if metadata is not None:
1694     result["VOL_METADATA"] = metadata
1695
1696   return result
1697
1698
1699 def _VolumeLogName(kind, es_name, volume):
1700   """Compute the ExtStorage log filename for a given Volume and operation.
1701
1702   @type kind: string
1703   @param kind: the operation type (e.g. create, remove etc.)
1704   @type es_name: string
1705   @param es_name: the ExtStorage name
1706   @type volume: string
1707   @param volume: the name of the Volume inside the External Storage
1708
1709   """
1710   # Check if the extstorage log dir is a valid dir
1711   if not os.path.isdir(pathutils.LOG_ES_DIR):
1712     base.ThrowError("Cannot find log directory: %s", pathutils.LOG_ES_DIR)
1713
1714   # TODO: Use tempfile.mkstemp to create unique filename
1715   basename = ("%s-%s-%s-%s.log" %
1716               (kind, es_name, volume, utils.TimestampForFilename()))
1717   return utils.PathJoin(pathutils.LOG_ES_DIR, basename)
1718
1719
1720 DEV_MAP = {
1721   constants.LD_LV: LogicalVolume,
1722   constants.LD_DRBD8: drbd.DRBD8Dev,
1723   constants.LD_BLOCKDEV: PersistentBlockDevice,
1724   constants.LD_RBD: RADOSBlockDevice,
1725   constants.LD_EXT: ExtStorageDevice,
1726   }
1727
1728 if constants.ENABLE_FILE_STORAGE or constants.ENABLE_SHARED_FILE_STORAGE:
1729   DEV_MAP[constants.LD_FILE] = FileStorage
1730
1731
1732 def _VerifyDiskType(dev_type):
1733   if dev_type not in DEV_MAP:
1734     raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1735
1736
1737 def _VerifyDiskParams(disk):
1738   """Verifies if all disk parameters are set.
1739
1740   """
1741   missing = set(constants.DISK_LD_DEFAULTS[disk.dev_type]) - set(disk.params)
1742   if missing:
1743     raise errors.ProgrammerError("Block device is missing disk parameters: %s" %
1744                                  missing)
1745
1746
1747 def FindDevice(disk, children):
1748   """Search for an existing, assembled device.
1749
1750   This will succeed only if the device exists and is assembled, but it
1751   does not do any actions in order to activate the device.
1752
1753   @type disk: L{objects.Disk}
1754   @param disk: the disk object to find
1755   @type children: list of L{bdev.BlockDev}
1756   @param children: the list of block devices that are children of the device
1757                   represented by the disk parameter
1758
1759   """
1760   _VerifyDiskType(disk.dev_type)
1761   device = DEV_MAP[disk.dev_type](disk.physical_id, children, disk.size,
1762                                   disk.params)
1763   if not device.attached:
1764     return None
1765   return device
1766
1767
1768 def Assemble(disk, children):
1769   """Try to attach or assemble an existing device.
1770
1771   This will attach to assemble the device, as needed, to bring it
1772   fully up. It must be safe to run on already-assembled devices.
1773
1774   @type disk: L{objects.Disk}
1775   @param disk: the disk object to assemble
1776   @type children: list of L{bdev.BlockDev}
1777   @param children: the list of block devices that are children of the device
1778                   represented by the disk parameter
1779
1780   """
1781   _VerifyDiskType(disk.dev_type)
1782   _VerifyDiskParams(disk)
1783   device = DEV_MAP[disk.dev_type](disk.physical_id, children, disk.size,
1784                                   disk.params)
1785   device.Assemble()
1786   return device
1787
1788
1789 def Create(disk, children, excl_stor):
1790   """Create a device.
1791
1792   @type disk: L{objects.Disk}
1793   @param disk: the disk object to create
1794   @type children: list of L{bdev.BlockDev}
1795   @param children: the list of block devices that are children of the device
1796                   represented by the disk parameter
1797   @type excl_stor: boolean
1798   @param excl_stor: Whether exclusive_storage is active
1799
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.params, excl_stor)
1805   return device