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