Attaching a logical volume builds a list of PVs
[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
750 class FileStorage(base.BlockDev):
751   """File device.
752
753   This class represents the a file storage backend device.
754
755   The unique_id for the file device is a (file_driver, file_path) tuple.
756
757   """
758   def __init__(self, unique_id, children, size, params):
759     """Initalizes a file device backend.
760
761     """
762     if children:
763       raise errors.BlockDeviceError("Invalid setup for file device")
764     super(FileStorage, self).__init__(unique_id, children, size, params)
765     if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
766       raise ValueError("Invalid configuration data %s" % str(unique_id))
767     self.driver = unique_id[0]
768     self.dev_path = unique_id[1]
769
770     CheckFileStoragePath(self.dev_path)
771
772     self.Attach()
773
774   def Assemble(self):
775     """Assemble the device.
776
777     Checks whether the file device exists, raises BlockDeviceError otherwise.
778
779     """
780     if not os.path.exists(self.dev_path):
781       base.ThrowError("File device '%s' does not exist" % self.dev_path)
782
783   def Shutdown(self):
784     """Shutdown the device.
785
786     This is a no-op for the file type, as we don't deactivate
787     the file on shutdown.
788
789     """
790     pass
791
792   def Open(self, force=False):
793     """Make the device ready for I/O.
794
795     This is a no-op for the file type.
796
797     """
798     pass
799
800   def Close(self):
801     """Notifies that the device will no longer be used for I/O.
802
803     This is a no-op for the file type.
804
805     """
806     pass
807
808   def Remove(self):
809     """Remove the file backing the block device.
810
811     @rtype: boolean
812     @return: True if the removal was successful
813
814     """
815     try:
816       os.remove(self.dev_path)
817     except OSError, err:
818       if err.errno != errno.ENOENT:
819         base.ThrowError("Can't remove file '%s': %s", self.dev_path, err)
820
821   def Rename(self, new_id):
822     """Renames the file.
823
824     """
825     # TODO: implement rename for file-based storage
826     base.ThrowError("Rename is not supported for file-based storage")
827
828   def Grow(self, amount, dryrun, backingstore):
829     """Grow the file
830
831     @param amount: the amount (in mebibytes) to grow with
832
833     """
834     if not backingstore:
835       return
836     # Check that the file exists
837     self.Assemble()
838     current_size = self.GetActualSize()
839     new_size = current_size + amount * 1024 * 1024
840     assert new_size > current_size, "Cannot Grow with a negative amount"
841     # We can't really simulate the growth
842     if dryrun:
843       return
844     try:
845       f = open(self.dev_path, "a+")
846       f.truncate(new_size)
847       f.close()
848     except EnvironmentError, err:
849       base.ThrowError("Error in file growth: %", str(err))
850
851   def Attach(self):
852     """Attach to an existing file.
853
854     Check if this file already exists.
855
856     @rtype: boolean
857     @return: True if file exists
858
859     """
860     self.attached = os.path.exists(self.dev_path)
861     return self.attached
862
863   def GetActualSize(self):
864     """Return the actual disk size.
865
866     @note: the device needs to be active when this is called
867
868     """
869     assert self.attached, "BlockDevice not attached in GetActualSize()"
870     try:
871       st = os.stat(self.dev_path)
872       return st.st_size
873     except OSError, err:
874       base.ThrowError("Can't stat %s: %s", self.dev_path, err)
875
876   @classmethod
877   def Create(cls, unique_id, children, size, spindles, params, excl_stor):
878     """Create a new file.
879
880     @param size: the size of file in MiB
881
882     @rtype: L{bdev.FileStorage}
883     @return: an instance of FileStorage
884
885     """
886     if excl_stor:
887       raise errors.ProgrammerError("FileStorage device requested with"
888                                    " exclusive_storage")
889     if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
890       raise ValueError("Invalid configuration data %s" % str(unique_id))
891
892     dev_path = unique_id[1]
893
894     CheckFileStoragePath(dev_path)
895
896     try:
897       fd = os.open(dev_path, os.O_RDWR | os.O_CREAT | os.O_EXCL)
898       f = os.fdopen(fd, "w")
899       f.truncate(size * 1024 * 1024)
900       f.close()
901     except EnvironmentError, err:
902       if err.errno == errno.EEXIST:
903         base.ThrowError("File already existing: %s", dev_path)
904       base.ThrowError("Error in file creation: %", str(err))
905
906     return FileStorage(unique_id, children, size, params)
907
908
909 class PersistentBlockDevice(base.BlockDev):
910   """A block device with persistent node
911
912   May be either directly attached, or exposed through DM (e.g. dm-multipath).
913   udev helpers are probably required to give persistent, human-friendly
914   names.
915
916   For the time being, pathnames are required to lie under /dev.
917
918   """
919   def __init__(self, unique_id, children, size, params):
920     """Attaches to a static block device.
921
922     The unique_id is a path under /dev.
923
924     """
925     super(PersistentBlockDevice, self).__init__(unique_id, children, size,
926                                                 params)
927     if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
928       raise ValueError("Invalid configuration data %s" % str(unique_id))
929     self.dev_path = unique_id[1]
930     if not os.path.realpath(self.dev_path).startswith("/dev/"):
931       raise ValueError("Full path '%s' lies outside /dev" %
932                               os.path.realpath(self.dev_path))
933     # TODO: this is just a safety guard checking that we only deal with devices
934     # we know how to handle. In the future this will be integrated with
935     # external storage backends and possible values will probably be collected
936     # from the cluster configuration.
937     if unique_id[0] != constants.BLOCKDEV_DRIVER_MANUAL:
938       raise ValueError("Got persistent block device of invalid type: %s" %
939                        unique_id[0])
940
941     self.major = self.minor = None
942     self.Attach()
943
944   @classmethod
945   def Create(cls, unique_id, children, size, spindles, params, excl_stor):
946     """Create a new device
947
948     This is a noop, we only return a PersistentBlockDevice instance
949
950     """
951     if excl_stor:
952       raise errors.ProgrammerError("Persistent block device requested with"
953                                    " exclusive_storage")
954     return PersistentBlockDevice(unique_id, children, 0, params)
955
956   def Remove(self):
957     """Remove a device
958
959     This is a noop
960
961     """
962     pass
963
964   def Rename(self, new_id):
965     """Rename this device.
966
967     """
968     base.ThrowError("Rename is not supported for PersistentBlockDev storage")
969
970   def Attach(self):
971     """Attach to an existing block device.
972
973
974     """
975     self.attached = False
976     try:
977       st = os.stat(self.dev_path)
978     except OSError, err:
979       logging.error("Error stat()'ing %s: %s", self.dev_path, str(err))
980       return False
981
982     if not stat.S_ISBLK(st.st_mode):
983       logging.error("%s is not a block device", self.dev_path)
984       return False
985
986     self.major = os.major(st.st_rdev)
987     self.minor = os.minor(st.st_rdev)
988     self.attached = True
989
990     return True
991
992   def Assemble(self):
993     """Assemble the device.
994
995     """
996     pass
997
998   def Shutdown(self):
999     """Shutdown the device.
1000
1001     """
1002     pass
1003
1004   def Open(self, force=False):
1005     """Make the device ready for I/O.
1006
1007     """
1008     pass
1009
1010   def Close(self):
1011     """Notifies that the device will no longer be used for I/O.
1012
1013     """
1014     pass
1015
1016   def Grow(self, amount, dryrun, backingstore):
1017     """Grow the logical volume.
1018
1019     """
1020     base.ThrowError("Grow is not supported for PersistentBlockDev storage")
1021
1022
1023 class RADOSBlockDevice(base.BlockDev):
1024   """A RADOS Block Device (rbd).
1025
1026   This class implements the RADOS Block Device for the backend. You need
1027   the rbd kernel driver, the RADOS Tools and a working RADOS cluster for
1028   this to be functional.
1029
1030   """
1031   def __init__(self, unique_id, children, size, params):
1032     """Attaches to an rbd device.
1033
1034     """
1035     super(RADOSBlockDevice, self).__init__(unique_id, children, size, params)
1036     if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1037       raise ValueError("Invalid configuration data %s" % str(unique_id))
1038
1039     self.driver, self.rbd_name = unique_id
1040
1041     self.major = self.minor = None
1042     self.Attach()
1043
1044   @classmethod
1045   def Create(cls, unique_id, children, size, spindles, params, excl_stor):
1046     """Create a new rbd device.
1047
1048     Provision a new rbd volume inside a RADOS pool.
1049
1050     """
1051     if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1052       raise errors.ProgrammerError("Invalid configuration data %s" %
1053                                    str(unique_id))
1054     if excl_stor:
1055       raise errors.ProgrammerError("RBD device requested with"
1056                                    " exclusive_storage")
1057     rbd_pool = params[constants.LDP_POOL]
1058     rbd_name = unique_id[1]
1059
1060     # Provision a new rbd volume (Image) inside the RADOS cluster.
1061     cmd = [constants.RBD_CMD, "create", "-p", rbd_pool,
1062            rbd_name, "--size", "%s" % size]
1063     result = utils.RunCmd(cmd)
1064     if result.failed:
1065       base.ThrowError("rbd creation failed (%s): %s",
1066                       result.fail_reason, result.output)
1067
1068     return RADOSBlockDevice(unique_id, children, size, params)
1069
1070   def Remove(self):
1071     """Remove the rbd device.
1072
1073     """
1074     rbd_pool = self.params[constants.LDP_POOL]
1075     rbd_name = self.unique_id[1]
1076
1077     if not self.minor and not self.Attach():
1078       # The rbd device doesn't exist.
1079       return
1080
1081     # First shutdown the device (remove mappings).
1082     self.Shutdown()
1083
1084     # Remove the actual Volume (Image) from the RADOS cluster.
1085     cmd = [constants.RBD_CMD, "rm", "-p", rbd_pool, rbd_name]
1086     result = utils.RunCmd(cmd)
1087     if result.failed:
1088       base.ThrowError("Can't remove Volume from cluster with rbd rm: %s - %s",
1089                       result.fail_reason, result.output)
1090
1091   def Rename(self, new_id):
1092     """Rename this device.
1093
1094     """
1095     pass
1096
1097   def Attach(self):
1098     """Attach to an existing rbd device.
1099
1100     This method maps the rbd volume that matches our name with
1101     an rbd device and then attaches to this device.
1102
1103     """
1104     self.attached = False
1105
1106     # Map the rbd volume to a block device under /dev
1107     self.dev_path = self._MapVolumeToBlockdev(self.unique_id)
1108
1109     try:
1110       st = os.stat(self.dev_path)
1111     except OSError, err:
1112       logging.error("Error stat()'ing %s: %s", self.dev_path, str(err))
1113       return False
1114
1115     if not stat.S_ISBLK(st.st_mode):
1116       logging.error("%s is not a block device", self.dev_path)
1117       return False
1118
1119     self.major = os.major(st.st_rdev)
1120     self.minor = os.minor(st.st_rdev)
1121     self.attached = True
1122
1123     return True
1124
1125   def _MapVolumeToBlockdev(self, unique_id):
1126     """Maps existing rbd volumes to block devices.
1127
1128     This method should be idempotent if the mapping already exists.
1129
1130     @rtype: string
1131     @return: the block device path that corresponds to the volume
1132
1133     """
1134     pool = self.params[constants.LDP_POOL]
1135     name = unique_id[1]
1136
1137     # Check if the mapping already exists.
1138     rbd_dev = self._VolumeToBlockdev(pool, name)
1139     if rbd_dev:
1140       # The mapping exists. Return it.
1141       return rbd_dev
1142
1143     # The mapping doesn't exist. Create it.
1144     map_cmd = [constants.RBD_CMD, "map", "-p", pool, name]
1145     result = utils.RunCmd(map_cmd)
1146     if result.failed:
1147       base.ThrowError("rbd map failed (%s): %s",
1148                       result.fail_reason, result.output)
1149
1150     # Find the corresponding rbd device.
1151     rbd_dev = self._VolumeToBlockdev(pool, name)
1152     if not rbd_dev:
1153       base.ThrowError("rbd map succeeded, but could not find the rbd block"
1154                       " device in output of showmapped, for volume: %s", name)
1155
1156     # The device was successfully mapped. Return it.
1157     return rbd_dev
1158
1159   @classmethod
1160   def _VolumeToBlockdev(cls, pool, volume_name):
1161     """Do the 'volume name'-to-'rbd block device' resolving.
1162
1163     @type pool: string
1164     @param pool: RADOS pool to use
1165     @type volume_name: string
1166     @param volume_name: the name of the volume whose device we search for
1167     @rtype: string or None
1168     @return: block device path if the volume is mapped, else None
1169
1170     """
1171     try:
1172       # Newer versions of the rbd tool support json output formatting. Use it
1173       # if available.
1174       showmap_cmd = [
1175         constants.RBD_CMD,
1176         "showmapped",
1177         "-p",
1178         pool,
1179         "--format",
1180         "json"
1181         ]
1182       result = utils.RunCmd(showmap_cmd)
1183       if result.failed:
1184         logging.error("rbd JSON output formatting returned error (%s): %s,"
1185                       "falling back to plain output parsing",
1186                       result.fail_reason, result.output)
1187         raise RbdShowmappedJsonError
1188
1189       return cls._ParseRbdShowmappedJson(result.output, volume_name)
1190     except RbdShowmappedJsonError:
1191       # For older versions of rbd, we have to parse the plain / text output
1192       # manually.
1193       showmap_cmd = [constants.RBD_CMD, "showmapped", "-p", pool]
1194       result = utils.RunCmd(showmap_cmd)
1195       if result.failed:
1196         base.ThrowError("rbd showmapped failed (%s): %s",
1197                         result.fail_reason, result.output)
1198
1199       return cls._ParseRbdShowmappedPlain(result.output, volume_name)
1200
1201   @staticmethod
1202   def _ParseRbdShowmappedJson(output, volume_name):
1203     """Parse the json output of `rbd showmapped'.
1204
1205     This method parses the json output of `rbd showmapped' and returns the rbd
1206     block device path (e.g. /dev/rbd0) that matches the given rbd volume.
1207
1208     @type output: string
1209     @param output: the json output of `rbd showmapped'
1210     @type volume_name: string
1211     @param volume_name: the name of the volume whose device we search for
1212     @rtype: string or None
1213     @return: block device path if the volume is mapped, else None
1214
1215     """
1216     try:
1217       devices = serializer.LoadJson(output)
1218     except ValueError, err:
1219       base.ThrowError("Unable to parse JSON data: %s" % err)
1220
1221     rbd_dev = None
1222     for d in devices.values(): # pylint: disable=E1103
1223       try:
1224         name = d["name"]
1225       except KeyError:
1226         base.ThrowError("'name' key missing from json object %s", devices)
1227
1228       if name == volume_name:
1229         if rbd_dev is not None:
1230           base.ThrowError("rbd volume %s is mapped more than once", volume_name)
1231
1232         rbd_dev = d["device"]
1233
1234     return rbd_dev
1235
1236   @staticmethod
1237   def _ParseRbdShowmappedPlain(output, volume_name):
1238     """Parse the (plain / text) output of `rbd showmapped'.
1239
1240     This method parses the output of `rbd showmapped' and returns
1241     the rbd block device path (e.g. /dev/rbd0) that matches the
1242     given rbd volume.
1243
1244     @type output: string
1245     @param output: the plain text output of `rbd showmapped'
1246     @type volume_name: string
1247     @param volume_name: the name of the volume whose device we search for
1248     @rtype: string or None
1249     @return: block device path if the volume is mapped, else None
1250
1251     """
1252     allfields = 5
1253     volumefield = 2
1254     devicefield = 4
1255
1256     lines = output.splitlines()
1257
1258     # Try parsing the new output format (ceph >= 0.55).
1259     splitted_lines = map(lambda l: l.split(), lines)
1260
1261     # Check for empty output.
1262     if not splitted_lines:
1263       return None
1264
1265     # Check showmapped output, to determine number of fields.
1266     field_cnt = len(splitted_lines[0])
1267     if field_cnt != allfields:
1268       # Parsing the new format failed. Fallback to parsing the old output
1269       # format (< 0.55).
1270       splitted_lines = map(lambda l: l.split("\t"), lines)
1271       if field_cnt != allfields:
1272         base.ThrowError("Cannot parse rbd showmapped output expected %s fields,"
1273                         " found %s", allfields, field_cnt)
1274
1275     matched_lines = \
1276       filter(lambda l: len(l) == allfields and l[volumefield] == volume_name,
1277              splitted_lines)
1278
1279     if len(matched_lines) > 1:
1280       base.ThrowError("rbd volume %s mapped more than once", volume_name)
1281
1282     if matched_lines:
1283       # rbd block device found. Return it.
1284       rbd_dev = matched_lines[0][devicefield]
1285       return rbd_dev
1286
1287     # The given volume is not mapped.
1288     return None
1289
1290   def Assemble(self):
1291     """Assemble the device.
1292
1293     """
1294     pass
1295
1296   def Shutdown(self):
1297     """Shutdown the device.
1298
1299     """
1300     if not self.minor and not self.Attach():
1301       # The rbd device doesn't exist.
1302       return
1303
1304     # Unmap the block device from the Volume.
1305     self._UnmapVolumeFromBlockdev(self.unique_id)
1306
1307     self.minor = None
1308     self.dev_path = None
1309
1310   def _UnmapVolumeFromBlockdev(self, unique_id):
1311     """Unmaps the rbd device from the Volume it is mapped.
1312
1313     Unmaps the rbd device from the Volume it was previously mapped to.
1314     This method should be idempotent if the Volume isn't mapped.
1315
1316     """
1317     pool = self.params[constants.LDP_POOL]
1318     name = unique_id[1]
1319
1320     # Check if the mapping already exists.
1321     rbd_dev = self._VolumeToBlockdev(pool, name)
1322
1323     if rbd_dev:
1324       # The mapping exists. Unmap the rbd device.
1325       unmap_cmd = [constants.RBD_CMD, "unmap", "%s" % rbd_dev]
1326       result = utils.RunCmd(unmap_cmd)
1327       if result.failed:
1328         base.ThrowError("rbd unmap failed (%s): %s",
1329                         result.fail_reason, result.output)
1330
1331   def Open(self, force=False):
1332     """Make the device ready for I/O.
1333
1334     """
1335     pass
1336
1337   def Close(self):
1338     """Notifies that the device will no longer be used for I/O.
1339
1340     """
1341     pass
1342
1343   def Grow(self, amount, dryrun, backingstore):
1344     """Grow the Volume.
1345
1346     @type amount: integer
1347     @param amount: the amount (in mebibytes) to grow with
1348     @type dryrun: boolean
1349     @param dryrun: whether to execute the operation in simulation mode
1350         only, without actually increasing the size
1351
1352     """
1353     if not backingstore:
1354       return
1355     if not self.Attach():
1356       base.ThrowError("Can't attach to rbd device during Grow()")
1357
1358     if dryrun:
1359       # the rbd tool does not support dry runs of resize operations.
1360       # Since rbd volumes are thinly provisioned, we assume
1361       # there is always enough free space for the operation.
1362       return
1363
1364     rbd_pool = self.params[constants.LDP_POOL]
1365     rbd_name = self.unique_id[1]
1366     new_size = self.size + amount
1367
1368     # Resize the rbd volume (Image) inside the RADOS cluster.
1369     cmd = [constants.RBD_CMD, "resize", "-p", rbd_pool,
1370            rbd_name, "--size", "%s" % new_size]
1371     result = utils.RunCmd(cmd)
1372     if result.failed:
1373       base.ThrowError("rbd resize failed (%s): %s",
1374                       result.fail_reason, result.output)
1375
1376
1377 class ExtStorageDevice(base.BlockDev):
1378   """A block device provided by an ExtStorage Provider.
1379
1380   This class implements the External Storage Interface, which means
1381   handling of the externally provided block devices.
1382
1383   """
1384   def __init__(self, unique_id, children, size, params):
1385     """Attaches to an extstorage block device.
1386
1387     """
1388     super(ExtStorageDevice, self).__init__(unique_id, children, size, params)
1389     if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1390       raise ValueError("Invalid configuration data %s" % str(unique_id))
1391
1392     self.driver, self.vol_name = unique_id
1393     self.ext_params = params
1394
1395     self.major = self.minor = None
1396     self.Attach()
1397
1398   @classmethod
1399   def Create(cls, unique_id, children, size, spindles, params, excl_stor):
1400     """Create a new extstorage device.
1401
1402     Provision a new volume using an extstorage provider, which will
1403     then be mapped to a block device.
1404
1405     """
1406     if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
1407       raise errors.ProgrammerError("Invalid configuration data %s" %
1408                                    str(unique_id))
1409     if excl_stor:
1410       raise errors.ProgrammerError("extstorage device requested with"
1411                                    " exclusive_storage")
1412
1413     # Call the External Storage's create script,
1414     # to provision a new Volume inside the External Storage
1415     _ExtStorageAction(constants.ES_ACTION_CREATE, unique_id,
1416                       params, str(size))
1417
1418     return ExtStorageDevice(unique_id, children, size, params)
1419
1420   def Remove(self):
1421     """Remove the extstorage device.
1422
1423     """
1424     if not self.minor and not self.Attach():
1425       # The extstorage device doesn't exist.
1426       return
1427
1428     # First shutdown the device (remove mappings).
1429     self.Shutdown()
1430
1431     # Call the External Storage's remove script,
1432     # to remove the Volume from the External Storage
1433     _ExtStorageAction(constants.ES_ACTION_REMOVE, self.unique_id,
1434                       self.ext_params)
1435
1436   def Rename(self, new_id):
1437     """Rename this device.
1438
1439     """
1440     pass
1441
1442   def Attach(self):
1443     """Attach to an existing extstorage device.
1444
1445     This method maps the extstorage volume that matches our name with
1446     a corresponding block device and then attaches to this device.
1447
1448     """
1449     self.attached = False
1450
1451     # Call the External Storage's attach script,
1452     # to attach an existing Volume to a block device under /dev
1453     self.dev_path = _ExtStorageAction(constants.ES_ACTION_ATTACH,
1454                                       self.unique_id, self.ext_params)
1455
1456     try:
1457       st = os.stat(self.dev_path)
1458     except OSError, err:
1459       logging.error("Error stat()'ing %s: %s", self.dev_path, str(err))
1460       return False
1461
1462     if not stat.S_ISBLK(st.st_mode):
1463       logging.error("%s is not a block device", self.dev_path)
1464       return False
1465
1466     self.major = os.major(st.st_rdev)
1467     self.minor = os.minor(st.st_rdev)
1468     self.attached = True
1469
1470     return True
1471
1472   def Assemble(self):
1473     """Assemble the device.
1474
1475     """
1476     pass
1477
1478   def Shutdown(self):
1479     """Shutdown the device.
1480
1481     """
1482     if not self.minor and not self.Attach():
1483       # The extstorage device doesn't exist.
1484       return
1485
1486     # Call the External Storage's detach script,
1487     # to detach an existing Volume from it's block device under /dev
1488     _ExtStorageAction(constants.ES_ACTION_DETACH, self.unique_id,
1489                       self.ext_params)
1490
1491     self.minor = None
1492     self.dev_path = None
1493
1494   def Open(self, force=False):
1495     """Make the device ready for I/O.
1496
1497     """
1498     pass
1499
1500   def Close(self):
1501     """Notifies that the device will no longer be used for I/O.
1502
1503     """
1504     pass
1505
1506   def Grow(self, amount, dryrun, backingstore):
1507     """Grow the Volume.
1508
1509     @type amount: integer
1510     @param amount: the amount (in mebibytes) to grow with
1511     @type dryrun: boolean
1512     @param dryrun: whether to execute the operation in simulation mode
1513         only, without actually increasing the size
1514
1515     """
1516     if not backingstore:
1517       return
1518     if not self.Attach():
1519       base.ThrowError("Can't attach to extstorage device during Grow()")
1520
1521     if dryrun:
1522       # we do not support dry runs of resize operations for now.
1523       return
1524
1525     new_size = self.size + amount
1526
1527     # Call the External Storage's grow script,
1528     # to grow an existing Volume inside the External Storage
1529     _ExtStorageAction(constants.ES_ACTION_GROW, self.unique_id,
1530                       self.ext_params, str(self.size), grow=str(new_size))
1531
1532   def SetInfo(self, text):
1533     """Update metadata with info text.
1534
1535     """
1536     # Replace invalid characters
1537     text = re.sub("^[^A-Za-z0-9_+.]", "_", text)
1538     text = re.sub("[^-A-Za-z0-9_+.]", "_", text)
1539
1540     # Only up to 128 characters are allowed
1541     text = text[:128]
1542
1543     # Call the External Storage's setinfo script,
1544     # to set metadata for an existing Volume inside the External Storage
1545     _ExtStorageAction(constants.ES_ACTION_SETINFO, self.unique_id,
1546                       self.ext_params, metadata=text)
1547
1548
1549 def _ExtStorageAction(action, unique_id, ext_params,
1550                       size=None, grow=None, metadata=None):
1551   """Take an External Storage action.
1552
1553   Take an External Storage action concerning or affecting
1554   a specific Volume inside the External Storage.
1555
1556   @type action: string
1557   @param action: which action to perform. One of:
1558                  create / remove / grow / attach / detach
1559   @type unique_id: tuple (driver, vol_name)
1560   @param unique_id: a tuple containing the type of ExtStorage (driver)
1561                     and the Volume name
1562   @type ext_params: dict
1563   @param ext_params: ExtStorage parameters
1564   @type size: integer
1565   @param size: the size of the Volume in mebibytes
1566   @type grow: integer
1567   @param grow: the new size in mebibytes (after grow)
1568   @type metadata: string
1569   @param metadata: metadata info of the Volume, for use by the provider
1570   @rtype: None or a block device path (during attach)
1571
1572   """
1573   driver, vol_name = unique_id
1574
1575   # Create an External Storage instance of type `driver'
1576   status, inst_es = ExtStorageFromDisk(driver)
1577   if not status:
1578     base.ThrowError("%s" % inst_es)
1579
1580   # Create the basic environment for the driver's scripts
1581   create_env = _ExtStorageEnvironment(unique_id, ext_params, size,
1582                                       grow, metadata)
1583
1584   # Do not use log file for action `attach' as we need
1585   # to get the output from RunResult
1586   # TODO: find a way to have a log file for attach too
1587   logfile = None
1588   if action is not constants.ES_ACTION_ATTACH:
1589     logfile = _VolumeLogName(action, driver, vol_name)
1590
1591   # Make sure the given action results in a valid script
1592   if action not in constants.ES_SCRIPTS:
1593     base.ThrowError("Action '%s' doesn't result in a valid ExtStorage script" %
1594                     action)
1595
1596   # Find out which external script to run according the given action
1597   script_name = action + "_script"
1598   script = getattr(inst_es, script_name)
1599
1600   # Run the external script
1601   result = utils.RunCmd([script], env=create_env,
1602                         cwd=inst_es.path, output=logfile,)
1603   if result.failed:
1604     logging.error("External storage's %s command '%s' returned"
1605                   " error: %s, logfile: %s, output: %s",
1606                   action, result.cmd, result.fail_reason,
1607                   logfile, result.output)
1608
1609     # If logfile is 'None' (during attach), it breaks TailFile
1610     # TODO: have a log file for attach too
1611     if action is not constants.ES_ACTION_ATTACH:
1612       lines = [utils.SafeEncode(val)
1613                for val in utils.TailFile(logfile, lines=20)]
1614     else:
1615       lines = result.output[-20:]
1616
1617     base.ThrowError("External storage's %s script failed (%s), last"
1618                     " lines of output:\n%s",
1619                     action, result.fail_reason, "\n".join(lines))
1620
1621   if action == constants.ES_ACTION_ATTACH:
1622     return result.stdout
1623
1624
1625 def ExtStorageFromDisk(name, base_dir=None):
1626   """Create an ExtStorage instance from disk.
1627
1628   This function will return an ExtStorage instance
1629   if the given name is a valid ExtStorage name.
1630
1631   @type base_dir: string
1632   @keyword base_dir: Base directory containing ExtStorage installations.
1633                      Defaults to a search in all the ES_SEARCH_PATH dirs.
1634   @rtype: tuple
1635   @return: True and the ExtStorage instance if we find a valid one, or
1636       False and the diagnose message on error
1637
1638   """
1639   if base_dir is None:
1640     es_base_dir = pathutils.ES_SEARCH_PATH
1641   else:
1642     es_base_dir = [base_dir]
1643
1644   es_dir = utils.FindFile(name, es_base_dir, os.path.isdir)
1645
1646   if es_dir is None:
1647     return False, ("Directory for External Storage Provider %s not"
1648                    " found in search path" % name)
1649
1650   # ES Files dictionary, we will populate it with the absolute path
1651   # names; if the value is True, then it is a required file, otherwise
1652   # an optional one
1653   es_files = dict.fromkeys(constants.ES_SCRIPTS, True)
1654
1655   es_files[constants.ES_PARAMETERS_FILE] = True
1656
1657   for (filename, _) in es_files.items():
1658     es_files[filename] = utils.PathJoin(es_dir, filename)
1659
1660     try:
1661       st = os.stat(es_files[filename])
1662     except EnvironmentError, err:
1663       return False, ("File '%s' under path '%s' is missing (%s)" %
1664                      (filename, es_dir, utils.ErrnoOrStr(err)))
1665
1666     if not stat.S_ISREG(stat.S_IFMT(st.st_mode)):
1667       return False, ("File '%s' under path '%s' is not a regular file" %
1668                      (filename, es_dir))
1669
1670     if filename in constants.ES_SCRIPTS:
1671       if stat.S_IMODE(st.st_mode) & stat.S_IXUSR != stat.S_IXUSR:
1672         return False, ("File '%s' under path '%s' is not executable" %
1673                        (filename, es_dir))
1674
1675   parameters = []
1676   if constants.ES_PARAMETERS_FILE in es_files:
1677     parameters_file = es_files[constants.ES_PARAMETERS_FILE]
1678     try:
1679       parameters = utils.ReadFile(parameters_file).splitlines()
1680     except EnvironmentError, err:
1681       return False, ("Error while reading the EXT parameters file at %s: %s" %
1682                      (parameters_file, utils.ErrnoOrStr(err)))
1683     parameters = [v.split(None, 1) for v in parameters]
1684
1685   es_obj = \
1686     objects.ExtStorage(name=name, path=es_dir,
1687                        create_script=es_files[constants.ES_SCRIPT_CREATE],
1688                        remove_script=es_files[constants.ES_SCRIPT_REMOVE],
1689                        grow_script=es_files[constants.ES_SCRIPT_GROW],
1690                        attach_script=es_files[constants.ES_SCRIPT_ATTACH],
1691                        detach_script=es_files[constants.ES_SCRIPT_DETACH],
1692                        setinfo_script=es_files[constants.ES_SCRIPT_SETINFO],
1693                        verify_script=es_files[constants.ES_SCRIPT_VERIFY],
1694                        supported_parameters=parameters)
1695   return True, es_obj
1696
1697
1698 def _ExtStorageEnvironment(unique_id, ext_params,
1699                            size=None, grow=None, metadata=None):
1700   """Calculate the environment for an External Storage script.
1701
1702   @type unique_id: tuple (driver, vol_name)
1703   @param unique_id: ExtStorage pool and name of the Volume
1704   @type ext_params: dict
1705   @param ext_params: the EXT parameters
1706   @type size: string
1707   @param size: size of the Volume (in mebibytes)
1708   @type grow: string
1709   @param grow: new size of Volume after grow (in mebibytes)
1710   @type metadata: string
1711   @param metadata: metadata info of the Volume
1712   @rtype: dict
1713   @return: dict of environment variables
1714
1715   """
1716   vol_name = unique_id[1]
1717
1718   result = {}
1719   result["VOL_NAME"] = vol_name
1720
1721   # EXT params
1722   for pname, pvalue in ext_params.items():
1723     result["EXTP_%s" % pname.upper()] = str(pvalue)
1724
1725   if size is not None:
1726     result["VOL_SIZE"] = size
1727
1728   if grow is not None:
1729     result["VOL_NEW_SIZE"] = grow
1730
1731   if metadata is not None:
1732     result["VOL_METADATA"] = metadata
1733
1734   return result
1735
1736
1737 def _VolumeLogName(kind, es_name, volume):
1738   """Compute the ExtStorage log filename for a given Volume and operation.
1739
1740   @type kind: string
1741   @param kind: the operation type (e.g. create, remove etc.)
1742   @type es_name: string
1743   @param es_name: the ExtStorage name
1744   @type volume: string
1745   @param volume: the name of the Volume inside the External Storage
1746
1747   """
1748   # Check if the extstorage log dir is a valid dir
1749   if not os.path.isdir(pathutils.LOG_ES_DIR):
1750     base.ThrowError("Cannot find log directory: %s", pathutils.LOG_ES_DIR)
1751
1752   # TODO: Use tempfile.mkstemp to create unique filename
1753   basename = ("%s-%s-%s-%s.log" %
1754               (kind, es_name, volume, utils.TimestampForFilename()))
1755   return utils.PathJoin(pathutils.LOG_ES_DIR, basename)
1756
1757
1758 DEV_MAP = {
1759   constants.LD_LV: LogicalVolume,
1760   constants.LD_DRBD8: drbd.DRBD8Dev,
1761   constants.LD_BLOCKDEV: PersistentBlockDevice,
1762   constants.LD_RBD: RADOSBlockDevice,
1763   constants.LD_EXT: ExtStorageDevice,
1764   }
1765
1766 if constants.ENABLE_FILE_STORAGE or constants.ENABLE_SHARED_FILE_STORAGE:
1767   DEV_MAP[constants.LD_FILE] = FileStorage
1768
1769
1770 def _VerifyDiskType(dev_type):
1771   if dev_type not in DEV_MAP:
1772     raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1773
1774
1775 def _VerifyDiskParams(disk):
1776   """Verifies if all disk parameters are set.
1777
1778   """
1779   missing = set(constants.DISK_LD_DEFAULTS[disk.dev_type]) - set(disk.params)
1780   if missing:
1781     raise errors.ProgrammerError("Block device is missing disk parameters: %s" %
1782                                  missing)
1783
1784
1785 def FindDevice(disk, children):
1786   """Search for an existing, assembled device.
1787
1788   This will succeed only if the device exists and is assembled, but it
1789   does not do any actions in order to activate the device.
1790
1791   @type disk: L{objects.Disk}
1792   @param disk: the disk object to find
1793   @type children: list of L{bdev.BlockDev}
1794   @param children: the list of block devices that are children of the device
1795                   represented by the disk parameter
1796
1797   """
1798   _VerifyDiskType(disk.dev_type)
1799   device = DEV_MAP[disk.dev_type](disk.physical_id, children, disk.size,
1800                                   disk.params)
1801   if not device.attached:
1802     return None
1803   return device
1804
1805
1806 def Assemble(disk, children):
1807   """Try to attach or assemble an existing device.
1808
1809   This will attach to assemble the device, as needed, to bring it
1810   fully up. It must be safe to run on already-assembled devices.
1811
1812   @type disk: L{objects.Disk}
1813   @param disk: the disk object to assemble
1814   @type children: list of L{bdev.BlockDev}
1815   @param children: the list of block devices that are children of the device
1816                   represented by the disk parameter
1817
1818   """
1819   _VerifyDiskType(disk.dev_type)
1820   _VerifyDiskParams(disk)
1821   device = DEV_MAP[disk.dev_type](disk.physical_id, children, disk.size,
1822                                   disk.params)
1823   device.Assemble()
1824   return device
1825
1826
1827 def Create(disk, children, excl_stor):
1828   """Create a device.
1829
1830   @type disk: L{objects.Disk}
1831   @param disk: the disk object to create
1832   @type children: list of L{bdev.BlockDev}
1833   @param children: the list of block devices that are children of the device
1834                   represented by the disk parameter
1835   @type excl_stor: boolean
1836   @param excl_stor: Whether exclusive_storage is active
1837   @rtype: L{bdev.BlockDev}
1838   @return: the created device, or C{None} in case of an error
1839
1840   """
1841   _VerifyDiskType(disk.dev_type)
1842   _VerifyDiskParams(disk)
1843   device = DEV_MAP[disk.dev_type].Create(disk.physical_id, children, disk.size,
1844                                          disk.spindles, disk.params, excl_stor)
1845   return device