root / lib / storage / base.py @ 24c06acb
History | View | Annotate | Download (11.6 kB)
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 __repr__(self): |
334 |
return ("<%s: unique_id: %s, children: %s, %s:%s, %s>" % |
335 |
(self.__class__, self.unique_id, self._children, |
336 |
self.major, self.minor, self.dev_path)) |
337 |
|
338 |
|
339 |
def ThrowError(msg, *args): |
340 |
"""Log an error to the node daemon and the raise an exception.
|
341 |
|
342 |
@type msg: string
|
343 |
@param msg: the text of the exception
|
344 |
@raise errors.BlockDeviceError
|
345 |
|
346 |
"""
|
347 |
if args:
|
348 |
msg = msg % args |
349 |
logging.error(msg) |
350 |
raise errors.BlockDeviceError(msg)
|
351 |
|
352 |
|
353 |
def IgnoreError(fn, *args, **kwargs): |
354 |
"""Executes the given function, ignoring BlockDeviceErrors.
|
355 |
|
356 |
This is used in order to simplify the execution of cleanup or
|
357 |
rollback functions.
|
358 |
|
359 |
@rtype: boolean
|
360 |
@return: True when fn didn't raise an exception, False otherwise
|
361 |
|
362 |
"""
|
363 |
try:
|
364 |
fn(*args, **kwargs) |
365 |
return True |
366 |
except errors.BlockDeviceError, err:
|
367 |
logging.warning("Caught BlockDeviceError but ignoring: %s", str(err)) |
368 |
return False |