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