7f731009930d04023e7429ebbf899dec4c53ad97
[ganeti-local] / lib / storage.py
1 #
2 #
3
4 # Copyright (C) 2009 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 """Storage container abstraction.
23
24 """
25
26
27 import logging
28
29 from ganeti import errors
30 from ganeti import constants
31 from ganeti import utils
32
33
34 def _ParseSize(value):
35   return int(round(float(value), 0))
36
37
38 class _Base:
39   """Base class for storage abstraction.
40
41   """
42   def List(self, name, fields):
43     """Returns a list of all entities within the storage unit.
44
45     @type name: string or None
46     @param name: Entity name or None for all
47     @type fields: list
48     @param fields: List with all requested result fields (order is preserved)
49
50     """
51     raise NotImplementedError()
52
53   def Modify(self, name, changes):
54     """Modifies an entity within the storage unit.
55
56     @type name: string
57     @param name: Entity name
58     @type changes: dict
59     @param changes: New field values
60
61     """
62     # Don't raise an error if no changes are requested
63     if changes:
64       raise errors.ProgrammerError("Unable to modify the following"
65                                    "fields: %r" % (changes.keys(), ))
66
67   def Execute(self, name, op):
68     """Executes an operation on an entity within the storage unit.
69
70     @type name: string
71     @param name: Entity name
72     @type op: string
73     @param op: Operation name
74
75     """
76     raise NotImplementedError()
77
78
79 class FileStorage(_Base):
80   """File storage unit.
81
82   """
83   def __init__(self, paths):
84     """Initializes this class.
85
86     @type paths: list
87     @param paths: List of file storage paths
88
89     """
90     self._paths = paths
91
92   def List(self, name, fields):
93     """Returns a list of all entities within the storage unit.
94
95     See L{_Base.List}.
96
97     """
98     rows = []
99
100     if name is None:
101       paths = self._paths
102     else:
103       paths = [name]
104
105     for path in paths:
106       rows.append(self._ListInner(path, fields))
107
108     return rows
109
110   @staticmethod
111   def _ListInner(path, fields):
112     """Gathers requested information from directory.
113
114     @type path: string
115     @param path: Path to directory
116     @type fields: list
117     @param fields: Requested fields
118
119     """
120     values = []
121
122     # Pre-calculate information in case it's requested more than once
123     if constants.SF_USED in fields:
124       dirsize = utils.CalculateDirectorySize(path)
125     else:
126       dirsize = None
127
128     if constants.SF_FREE in fields or constants.SF_SIZE in fields:
129       fsstats = utils.GetFilesystemStats(path)
130     else:
131       fsstats = None
132
133     # Make sure to update constants.VALID_STORAGE_FIELDS when changing fields.
134     for field_name in fields:
135       if field_name == constants.SF_NAME:
136         values.append(path)
137
138       elif field_name == constants.SF_USED:
139         values.append(dirsize)
140
141       elif field_name == constants.SF_FREE:
142         values.append(fsstats[1])
143
144       elif field_name == constants.SF_SIZE:
145         values.append(fsstats[0])
146
147       elif field_name == constants.SF_ALLOCATABLE:
148         values.append(True)
149
150       else:
151         raise errors.StorageError("Unknown field: %r" % field_name)
152
153     return values
154
155
156 class _LvmBase(_Base):
157   """Base class for LVM storage containers.
158
159   @cvar LIST_FIELDS: list of tuples consisting of three elements: SF_*
160       constants, lvm command output fields (list), and conversion
161       function or static value (for static value, the lvm output field
162       can be an empty list)
163
164   """
165   LIST_SEP = "|"
166   LIST_COMMAND = None
167   LIST_FIELDS = None
168
169   def List(self, name, wanted_field_names):
170     """Returns a list of all entities within the storage unit.
171
172     See L{_Base.List}.
173
174     """
175     # Get needed LVM fields
176     lvm_fields = self._GetLvmFields(self.LIST_FIELDS, wanted_field_names)
177
178     # Build LVM command
179     cmd_args = self._BuildListCommand(self.LIST_COMMAND, self.LIST_SEP,
180                                       lvm_fields, name)
181
182     # Run LVM command
183     cmd_result = self._RunListCommand(cmd_args)
184
185     # Split and rearrange LVM command output
186     return self._BuildList(self._SplitList(cmd_result, self.LIST_SEP,
187                                            len(lvm_fields)),
188                            self.LIST_FIELDS,
189                            wanted_field_names,
190                            lvm_fields)
191
192   @staticmethod
193   def _GetLvmFields(fields_def, wanted_field_names):
194     """Returns unique list of fields wanted from LVM command.
195
196     @type fields_def: list
197     @param fields_def: Field definitions
198     @type wanted_field_names: list
199     @param wanted_field_names: List of requested fields
200
201     """
202     field_to_idx = dict([(field_name, idx)
203                          for (idx, (field_name, _, _)) in
204                          enumerate(fields_def)])
205
206     lvm_fields = []
207
208     for field_name in wanted_field_names:
209       try:
210         idx = field_to_idx[field_name]
211       except IndexError:
212         raise errors.StorageError("Unknown field: %r" % field_name)
213
214       (_, lvm_names, _) = fields_def[idx]
215
216       lvm_fields.extend(lvm_names)
217
218     return utils.UniqueSequence(lvm_fields)
219
220   @classmethod
221   def _BuildList(cls, cmd_result, fields_def, wanted_field_names, lvm_fields):
222     """Builds the final result list.
223
224     @type cmd_result: iterable
225     @param cmd_result: Iterable of LVM command output (iterable of lists)
226     @type fields_def: list
227     @param fields_def: Field definitions
228     @type wanted_field_names: list
229     @param wanted_field_names: List of requested fields
230     @type lvm_fields: list
231     @param lvm_fields: LVM fields
232
233     """
234     lvm_name_to_idx = dict([(lvm_name, idx)
235                            for (idx, lvm_name) in enumerate(lvm_fields)])
236     field_to_idx = dict([(field_name, idx)
237                          for (idx, (field_name, _, _)) in
238                          enumerate(fields_def)])
239
240     data = []
241     for raw_data in cmd_result:
242       row = []
243
244       for field_name in wanted_field_names:
245         (_, lvm_names, mapper) = fields_def[field_to_idx[field_name]]
246
247         values = [raw_data[lvm_name_to_idx[i]] for i in lvm_names]
248
249         if callable(mapper):
250           # we got a function, call it with all the declared fields
251           val = mapper(*values)
252         elif len(values) == 1:
253           # we don't have a function, but we had a single field
254           # declared, pass it unchanged
255           val = values[0]
256         else:
257           # let's make sure there are no fields declared (cannot map >
258           # 1 field without a function)
259           assert not values, "LVM storage has multi-fields without a function"
260           val = mapper
261
262         row.append(val)
263
264       data.append(row)
265
266     return data
267
268   @staticmethod
269   def _BuildListCommand(cmd, sep, options, name):
270     """Builds LVM command line.
271
272     @type cmd: string
273     @param cmd: Command name
274     @type sep: string
275     @param sep: Field separator character
276     @type options: list of strings
277     @param options: Wanted LVM fields
278     @type name: name or None
279     @param name: Name of requested entity
280
281     """
282     args = [cmd,
283             "--noheadings", "--units=m", "--nosuffix",
284             "--separator", sep,
285             "--options", ",".join(options)]
286
287     if name is not None:
288       args.append(name)
289
290     return args
291
292   @staticmethod
293   def _RunListCommand(args):
294     """Run LVM command.
295
296     """
297     result = utils.RunCmd(args)
298
299     if result.failed:
300       raise errors.StorageError("Failed to run %r, command output: %s" %
301                                 (args[0], result.output))
302
303     return result.stdout
304
305   @staticmethod
306   def _SplitList(data, sep, fieldcount):
307     """Splits LVM command output into rows and fields.
308
309     @type data: string
310     @param data: LVM command output
311     @type sep: string
312     @param sep: Field separator character
313     @type fieldcount: int
314     @param fieldcount: Expected number of fields
315
316     """
317     for line in data.splitlines():
318       fields = line.strip().split(sep)
319
320       if len(fields) != fieldcount:
321         logging.warning("Invalid line returned from lvm command: %s", line)
322         continue
323
324       yield fields
325
326
327 class LvmPvStorage(_LvmBase):
328   """LVM Physical Volume storage unit.
329
330   """
331   def _GetAllocatable(attr):
332     if attr:
333       return (attr[0] == "a")
334     else:
335       logging.warning("Invalid PV attribute: %r", attr)
336       return False
337
338   LIST_COMMAND = "pvs"
339
340   # Make sure to update constants.VALID_STORAGE_FIELDS when changing field
341   # definitions.
342   LIST_FIELDS = [
343     (constants.SF_NAME, ["pv_name"], None),
344     (constants.SF_SIZE, ["pv_size"], _ParseSize),
345     (constants.SF_USED, ["pv_used"], _ParseSize),
346     (constants.SF_FREE, ["pv_free"], _ParseSize),
347     (constants.SF_ALLOCATABLE, ["pv_attr"], _GetAllocatable),
348     ]
349
350   def _SetAllocatable(self, name, allocatable):
351     """Sets the "allocatable" flag on a physical volume.
352
353     @type name: string
354     @param name: Physical volume name
355     @type allocatable: bool
356     @param allocatable: Whether to set the "allocatable" flag
357
358     """
359     args = ["pvchange", "--allocatable"]
360
361     if allocatable:
362       args.append("y")
363     else:
364       args.append("n")
365
366     args.append(name)
367
368     result = utils.RunCmd(args)
369     if result.failed:
370       raise errors.StorageError("Failed to modify physical volume,"
371                                 " pvchange output: %s" %
372                                 result.output)
373
374   def Modify(self, name, changes):
375     """Modifies flags on a physical volume.
376
377     See L{_Base.Modify}.
378
379     """
380     if constants.SF_ALLOCATABLE in changes:
381       self._SetAllocatable(name, changes[constants.SF_ALLOCATABLE])
382       del changes[constants.SF_ALLOCATABLE]
383
384     # Other changes will be handled (and maybe refused) by the base class.
385     return _LvmBase.Modify(self, name, changes)
386
387
388 class LvmVgStorage(_LvmBase):
389   """LVM Volume Group storage unit.
390
391   """
392   LIST_COMMAND = "vgs"
393
394   # Make sure to update constants.VALID_STORAGE_FIELDS when changing field
395   # definitions.
396   LIST_FIELDS = [
397     (constants.SF_NAME, ["vg_name"], None),
398     (constants.SF_SIZE, ["vg_size"], _ParseSize),
399     (constants.SF_FREE, ["vg_free"], _ParseSize),
400     (constants.SF_USED, ["vg_size", "vg_free"],
401      lambda x, y: _ParseSize(x) - _ParseSize(y)),
402     (constants.SF_ALLOCATABLE, [], True),
403     ]
404
405   def _RemoveMissing(self, name):
406     """Runs "vgreduce --removemissing" on a volume group.
407
408     @type name: string
409     @param name: Volume group name
410
411     """
412     # Ignoring vgreduce exit code. Older versions exit with an error even tough
413     # the VG is already consistent. This was fixed in later versions, but we
414     # cannot depend on it.
415     result = utils.RunCmd(["vgreduce", "--removemissing", name])
416
417     # Keep output in case something went wrong
418     vgreduce_output = result.output
419
420     result = utils.RunCmd(["vgs", "--noheadings", "--nosuffix", name])
421     if result.failed:
422       raise errors.StorageError(("Volume group '%s' still not consistent,"
423                                  " 'vgreduce' output: %r,"
424                                  " 'vgs' output: %r") %
425                                 (name, vgreduce_output, result.output))
426
427   def Execute(self, name, op):
428     """Executes an operation on a virtual volume.
429
430     See L{_Base.Execute}.
431
432     """
433     if op == constants.SO_FIX_CONSISTENCY:
434       return self._RemoveMissing(name)
435
436     return _LvmBase.Execute(self, name, op)
437
438
439 # Lookup table for storage types
440 _STORAGE_TYPES = {
441   constants.ST_FILE: FileStorage,
442   constants.ST_LVM_PV: LvmPvStorage,
443   constants.ST_LVM_VG: LvmVgStorage,
444   }
445
446
447 def GetStorageClass(name):
448   """Returns the class for a storage type.
449
450   @type name: string
451   @param name: Storage type
452
453   """
454   try:
455     return _STORAGE_TYPES[name]
456   except KeyError:
457     raise errors.StorageError("Unknown storage type: %r" % name)
458
459
460 def GetStorage(name, *args):
461   """Factory function for storage methods.
462
463   @type name: string
464   @param name: Storage type
465
466   """
467   return GetStorageClass(name)(*args)