New BlockDev methods to get spindles
[ganeti-local] / lib / storage / base.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 - base class and utility functions"""
23
24 import logging
25
26 from ganeti import objects
27 from ganeti import constants
28 from ganeti import utils
29 from ganeti import errors
30
31
32 class BlockDev(object):
33   """Block device abstract class.
34
35   A block device can be in the following states:
36     - not existing on the system, and by `Create()` it goes into:
37     - existing but not setup/not active, and by `Assemble()` goes into:
38     - active read-write and by `Open()` it goes into
39     - online (=used, or ready for use)
40
41   A device can also be online but read-only, however we are not using
42   the readonly state (LV has it, if needed in the future) and we are
43   usually looking at this like at a stack, so it's easier to
44   conceptualise the transition from not-existing to online and back
45   like a linear one.
46
47   The many different states of the device are due to the fact that we
48   need to cover many device types:
49     - logical volumes are created, lvchange -a y $lv, and used
50     - drbd devices are attached to a local disk/remote peer and made primary
51
52   A block device is identified by three items:
53     - the /dev path of the device (dynamic)
54     - a unique ID of the device (static)
55     - it's major/minor pair (dynamic)
56
57   Not all devices implement both the first two as distinct items. LVM
58   logical volumes have their unique ID (the pair volume group, logical
59   volume name) in a 1-to-1 relation to the dev path. For DRBD devices,
60   the /dev path is again dynamic and the unique id is the pair (host1,
61   dev1), (host2, dev2).
62
63   You can get to a device in two ways:
64     - creating the (real) device, which returns you
65       an attached instance (lvcreate)
66     - attaching of a python instance to an existing (real) device
67
68   The second point, the attachment to a device, is different
69   depending on whether the device is assembled or not. At init() time,
70   we search for a device with the same unique_id as us. If found,
71   good. It also means that the device is already assembled. If not,
72   after assembly we'll have our correct major/minor.
73
74   """
75   def __init__(self, unique_id, children, size, params):
76     self._children = children
77     self.dev_path = None
78     self.unique_id = unique_id
79     self.major = None
80     self.minor = None
81     self.attached = False
82     self.size = size
83     self.params = params
84
85   def Assemble(self):
86     """Assemble the device from its components.
87
88     Implementations of this method by child classes must ensure that:
89       - after the device has been assembled, it knows its major/minor
90         numbers; this allows other devices (usually parents) to probe
91         correctly for their children
92       - calling this method on an existing, in-use device is safe
93       - if the device is already configured (and in an OK state),
94         this method is idempotent
95
96     """
97     pass
98
99   def Attach(self):
100     """Find a device which matches our config and attach to it.
101
102     """
103     raise NotImplementedError
104
105   def Close(self):
106     """Notifies that the device will no longer be used for I/O.
107
108     """
109     raise NotImplementedError
110
111   @classmethod
112   def Create(cls, unique_id, children, size, spindles, params, excl_stor):
113     """Create the device.
114
115     If the device cannot be created, it will return None
116     instead. Error messages go to the logging system.
117
118     Note that for some devices, the unique_id is used, and for other,
119     the children. The idea is that these two, taken together, are
120     enough for both creation and assembly (later).
121
122     @type unique_id: 2-element tuple or list
123     @param unique_id: unique identifier; the details depend on the actual device
124         type
125     @type children: list of L{BlockDev}
126     @param children: for hierarchical devices, the child devices
127     @type size: float
128     @param size: size in MiB
129     @type spindles: int
130     @param spindles: number of physical disk to dedicate to the device
131     @type params: dict
132     @param params: device-specific options/parameters
133     @type excl_stor: bool
134     @param excl_stor: whether exclusive_storage is active
135     @rtype: L{BlockDev}
136     @return: the created device, or C{None} in case of an error
137
138     """
139     raise NotImplementedError
140
141   def Remove(self):
142     """Remove this device.
143
144     This makes sense only for some of the device types: LV and file
145     storage. Also note that if the device can't attach, the removal
146     can't be completed.
147
148     """
149     raise NotImplementedError
150
151   def Rename(self, new_id):
152     """Rename this device.
153
154     This may or may not make sense for a given device type.
155
156     """
157     raise NotImplementedError
158
159   def Open(self, force=False):
160     """Make the device ready for use.
161
162     This makes the device ready for I/O. For now, just the DRBD
163     devices need this.
164
165     The force parameter signifies that if the device has any kind of
166     --force thing, it should be used, we know what we are doing.
167
168     @type force: boolean
169
170     """
171     raise NotImplementedError
172
173   def Shutdown(self):
174     """Shut down the device, freeing its children.
175
176     This undoes the `Assemble()` work, except for the child
177     assembling; as such, the children on the device are still
178     assembled after this call.
179
180     """
181     raise NotImplementedError
182
183   def SetSyncParams(self, params):
184     """Adjust the synchronization parameters of the mirror.
185
186     In case this is not a mirroring device, this is no-op.
187
188     @param params: dictionary of LD level disk parameters related to the
189     synchronization.
190     @rtype: list
191     @return: a list of error messages, emitted both by the current node and by
192     children. An empty list means no errors.
193
194     """
195     result = []
196     if self._children:
197       for child in self._children:
198         result.extend(child.SetSyncParams(params))
199     return result
200
201   def PauseResumeSync(self, pause):
202     """Pause/Resume the sync of the mirror.
203
204     In case this is not a mirroring device, this is no-op.
205
206     @type pause: boolean
207     @param pause: Whether to pause or resume
208
209     """
210     result = True
211     if self._children:
212       for child in self._children:
213         result = result and child.PauseResumeSync(pause)
214     return result
215
216   def GetSyncStatus(self):
217     """Returns the sync status of the device.
218
219     If this device is a mirroring device, this function returns the
220     status of the mirror.
221
222     If sync_percent is None, it means the device is not syncing.
223
224     If estimated_time is None, it means we can't estimate
225     the time needed, otherwise it's the time left in seconds.
226
227     If is_degraded is True, it means the device is missing
228     redundancy. This is usually a sign that something went wrong in
229     the device setup, if sync_percent is None.
230
231     The ldisk parameter represents the degradation of the local
232     data. This is only valid for some devices, the rest will always
233     return False (not degraded).
234
235     @rtype: objects.BlockDevStatus
236
237     """
238     return objects.BlockDevStatus(dev_path=self.dev_path,
239                                   major=self.major,
240                                   minor=self.minor,
241                                   sync_percent=None,
242                                   estimated_time=None,
243                                   is_degraded=False,
244                                   ldisk_status=constants.LDS_OKAY)
245
246   def CombinedSyncStatus(self):
247     """Calculate the mirror status recursively for our children.
248
249     The return value is the same as for `GetSyncStatus()` except the
250     minimum percent and maximum time are calculated across our
251     children.
252
253     @rtype: objects.BlockDevStatus
254
255     """
256     status = self.GetSyncStatus()
257
258     min_percent = status.sync_percent
259     max_time = status.estimated_time
260     is_degraded = status.is_degraded
261     ldisk_status = status.ldisk_status
262
263     if self._children:
264       for child in self._children:
265         child_status = child.GetSyncStatus()
266
267         if min_percent is None:
268           min_percent = child_status.sync_percent
269         elif child_status.sync_percent is not None:
270           min_percent = min(min_percent, child_status.sync_percent)
271
272         if max_time is None:
273           max_time = child_status.estimated_time
274         elif child_status.estimated_time is not None:
275           max_time = max(max_time, child_status.estimated_time)
276
277         is_degraded = is_degraded or child_status.is_degraded
278
279         if ldisk_status is None:
280           ldisk_status = child_status.ldisk_status
281         elif child_status.ldisk_status is not None:
282           ldisk_status = max(ldisk_status, child_status.ldisk_status)
283
284     return objects.BlockDevStatus(dev_path=self.dev_path,
285                                   major=self.major,
286                                   minor=self.minor,
287                                   sync_percent=min_percent,
288                                   estimated_time=max_time,
289                                   is_degraded=is_degraded,
290                                   ldisk_status=ldisk_status)
291
292   def SetInfo(self, text):
293     """Update metadata with info text.
294
295     Only supported for some device types.
296
297     """
298     for child in self._children:
299       child.SetInfo(text)
300
301   def Grow(self, amount, dryrun, backingstore):
302     """Grow the block device.
303
304     @type amount: integer
305     @param amount: the amount (in mebibytes) to grow with
306     @type dryrun: boolean
307     @param dryrun: whether to execute the operation in simulation mode
308         only, without actually increasing the size
309     @param backingstore: whether to execute the operation on backing storage
310         only, or on "logical" storage only; e.g. DRBD is logical storage,
311         whereas LVM, file, RBD are backing storage
312
313     """
314     raise NotImplementedError
315
316   def GetActualSize(self):
317     """Return the actual disk size.
318
319     @note: the device needs to be active when this is called
320
321     """
322     assert self.attached, "BlockDevice not attached in GetActualSize()"
323     result = utils.RunCmd(["blockdev", "--getsize64", self.dev_path])
324     if result.failed:
325       ThrowError("blockdev failed (%s): %s",
326                   result.fail_reason, result.output)
327     try:
328       sz = int(result.output.strip())
329     except (ValueError, TypeError), err:
330       ThrowError("Failed to parse blockdev output: %s", str(err))
331     return sz
332
333   def GetActualSpindles(self):
334     """Return the actual number of spindles used.
335
336     This is not supported by all devices; if not supported, C{None} is returned.
337
338     @note: the device needs to be active when this is called
339
340     """
341     assert self.attached, "BlockDevice not attached in GetActualSpindles()"
342     return None
343
344   def GetActualDimensions(self):
345     """Return the actual disk size and number of spindles used.
346
347     @rtype: tuple
348     @return: (size, spindles); spindles is C{None} when they are not supported
349
350     @note: the device needs to be active when this is called
351
352     """
353     return (self.GetActualSize(), self.GetActualSpindles())
354
355   def __repr__(self):
356     return ("<%s: unique_id: %s, children: %s, %s:%s, %s>" %
357             (self.__class__, self.unique_id, self._children,
358              self.major, self.minor, self.dev_path))
359
360
361 def ThrowError(msg, *args):
362   """Log an error to the node daemon and the raise an exception.
363
364   @type msg: string
365   @param msg: the text of the exception
366   @raise errors.BlockDeviceError
367
368   """
369   if args:
370     msg = msg % args
371   logging.error(msg)
372   raise errors.BlockDeviceError(msg)
373
374
375 def IgnoreError(fn, *args, **kwargs):
376   """Executes the given function, ignoring BlockDeviceErrors.
377
378   This is used in order to simplify the execution of cleanup or
379   rollback functions.
380
381   @rtype: boolean
382   @return: True when fn didn't raise an exception, False otherwise
383
384   """
385   try:
386     fn(*args, **kwargs)
387     return True
388   except errors.BlockDeviceError, err:
389     logging.warning("Caught BlockDeviceError but ignoring: %s", str(err))
390     return False