root / lib / storage / extstorage.py @ 7d81bb8b
History | View | Annotate | Download (15.9 kB)
1 |
#
|
---|---|
2 |
#
|
3 |
|
4 |
# Copyright (C) 2014 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 |
"""ExtStorage Interface related functionality
|
23 |
|
24 |
"""
|
25 |
|
26 |
import re |
27 |
import stat |
28 |
import os |
29 |
import logging |
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 pathutils |
36 |
from ganeti.storage import base |
37 |
|
38 |
|
39 |
class ExtStorageDevice(base.BlockDev): |
40 |
"""A block device provided by an ExtStorage Provider.
|
41 |
|
42 |
This class implements the External Storage Interface, which means
|
43 |
handling of the externally provided block devices.
|
44 |
|
45 |
"""
|
46 |
def __init__(self, unique_id, children, size, params, dyn_params, *args): |
47 |
"""Attaches to an extstorage block device.
|
48 |
|
49 |
"""
|
50 |
super(ExtStorageDevice, self).__init__(unique_id, children, size, params, |
51 |
dyn_params, *args) |
52 |
(self.name, self.uuid) = args |
53 |
|
54 |
if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2: |
55 |
raise ValueError("Invalid configuration data %s" % str(unique_id)) |
56 |
|
57 |
self.driver, self.vol_name = unique_id |
58 |
self.ext_params = params
|
59 |
|
60 |
self.major = self.minor = None |
61 |
self.uris = []
|
62 |
self.Attach()
|
63 |
|
64 |
@classmethod
|
65 |
def Create(cls, unique_id, children, size, spindles, params, excl_stor, |
66 |
dyn_params, *args): |
67 |
"""Create a new extstorage device.
|
68 |
|
69 |
Provision a new volume using an extstorage provider, which will
|
70 |
then be mapped to a block device.
|
71 |
|
72 |
"""
|
73 |
(name, uuid) = args |
74 |
|
75 |
if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2: |
76 |
raise errors.ProgrammerError("Invalid configuration data %s" % |
77 |
str(unique_id))
|
78 |
if excl_stor:
|
79 |
raise errors.ProgrammerError("extstorage device requested with" |
80 |
" exclusive_storage")
|
81 |
|
82 |
# Call the External Storage's create script,
|
83 |
# to provision a new Volume inside the External Storage
|
84 |
_ExtStorageAction(constants.ES_ACTION_CREATE, unique_id, |
85 |
params, size=str(size), name=name, uuid=uuid)
|
86 |
|
87 |
return ExtStorageDevice(unique_id, children, size, params, dyn_params,
|
88 |
*args) |
89 |
|
90 |
def Remove(self): |
91 |
"""Remove the extstorage device.
|
92 |
|
93 |
"""
|
94 |
if not self.minor and not self.Attach(): |
95 |
# The extstorage device doesn't exist.
|
96 |
return
|
97 |
|
98 |
# First shutdown the device (remove mappings).
|
99 |
self.Shutdown()
|
100 |
|
101 |
# Call the External Storage's remove script,
|
102 |
# to remove the Volume from the External Storage
|
103 |
_ExtStorageAction(constants.ES_ACTION_REMOVE, self.unique_id,
|
104 |
self.ext_params, name=self.name, uuid=self.uuid) |
105 |
|
106 |
def Rename(self, new_id): |
107 |
"""Rename this device.
|
108 |
|
109 |
"""
|
110 |
pass
|
111 |
|
112 |
def Attach(self): |
113 |
"""Attach to an existing extstorage device.
|
114 |
|
115 |
This method maps the extstorage volume that matches our name with
|
116 |
a corresponding block device and then attaches to this device.
|
117 |
|
118 |
"""
|
119 |
self.attached = False |
120 |
|
121 |
# Call the External Storage's attach script,
|
122 |
# to attach an existing Volume to a block device under /dev
|
123 |
result = _ExtStorageAction(constants.ES_ACTION_ATTACH, |
124 |
self.unique_id, self.ext_params, |
125 |
name=self.name, uuid=self.uuid) |
126 |
|
127 |
# Attach script returns the block device path and optionally
|
128 |
# the URIs to be used for userspace access (one URI for
|
129 |
# each hypervisor supported).
|
130 |
# If the provider doesn't support userspace access, then
|
131 |
# the 'uris' variable will be an empty list.
|
132 |
result = result.split("\n")
|
133 |
self.dev_path = result[0] |
134 |
self.uris = result[1:] |
135 |
|
136 |
# Verify that dev_path exists and is a block device
|
137 |
try:
|
138 |
st = os.stat(self.dev_path)
|
139 |
except OSError, err: |
140 |
logging.error("Error stat()'ing %s: %s", self.dev_path, str(err)) |
141 |
return False |
142 |
|
143 |
if not stat.S_ISBLK(st.st_mode): |
144 |
logging.error("%s is not a block device", self.dev_path) |
145 |
return False |
146 |
|
147 |
self.major = os.major(st.st_rdev)
|
148 |
self.minor = os.minor(st.st_rdev)
|
149 |
self.attached = True |
150 |
|
151 |
return True |
152 |
|
153 |
def Assemble(self): |
154 |
"""Assemble the device.
|
155 |
|
156 |
"""
|
157 |
pass
|
158 |
|
159 |
def Shutdown(self): |
160 |
"""Shutdown the device.
|
161 |
|
162 |
"""
|
163 |
if not self.minor and not self.Attach(): |
164 |
# The extstorage device doesn't exist.
|
165 |
return
|
166 |
|
167 |
# Call the External Storage's detach script,
|
168 |
# to detach an existing Volume from it's block device under /dev
|
169 |
_ExtStorageAction(constants.ES_ACTION_DETACH, self.unique_id,
|
170 |
self.ext_params, name=self.name, uuid=self.uuid) |
171 |
|
172 |
self.minor = None |
173 |
self.dev_path = None |
174 |
|
175 |
def Open(self, force=False): |
176 |
"""Make the device ready for I/O.
|
177 |
|
178 |
"""
|
179 |
pass
|
180 |
|
181 |
def Close(self): |
182 |
"""Notifies that the device will no longer be used for I/O.
|
183 |
|
184 |
"""
|
185 |
pass
|
186 |
|
187 |
def Grow(self, amount, dryrun, backingstore, excl_stor): |
188 |
"""Grow the Volume.
|
189 |
|
190 |
@type amount: integer
|
191 |
@param amount: the amount (in mebibytes) to grow with
|
192 |
@type dryrun: boolean
|
193 |
@param dryrun: whether to execute the operation in simulation mode
|
194 |
only, without actually increasing the size
|
195 |
|
196 |
"""
|
197 |
if not backingstore: |
198 |
return
|
199 |
if not self.Attach(): |
200 |
base.ThrowError("Can't attach to extstorage device during Grow()")
|
201 |
|
202 |
if dryrun:
|
203 |
# we do not support dry runs of resize operations for now.
|
204 |
return
|
205 |
|
206 |
new_size = self.size + amount
|
207 |
|
208 |
# Call the External Storage's grow script,
|
209 |
# to grow an existing Volume inside the External Storage
|
210 |
_ExtStorageAction(constants.ES_ACTION_GROW, self.unique_id,
|
211 |
self.ext_params, size=str(self.size), grow=str(new_size), |
212 |
name=self.name, uuid=self.uuid) |
213 |
|
214 |
def SetInfo(self, text): |
215 |
"""Update metadata with info text.
|
216 |
|
217 |
"""
|
218 |
# Replace invalid characters
|
219 |
text = re.sub("^[^A-Za-z0-9_+.]", "_", text) |
220 |
text = re.sub("[^-A-Za-z0-9_+.]", "_", text) |
221 |
|
222 |
# Only up to 128 characters are allowed
|
223 |
text = text[:128]
|
224 |
|
225 |
# Call the External Storage's setinfo script,
|
226 |
# to set metadata for an existing Volume inside the External Storage
|
227 |
_ExtStorageAction(constants.ES_ACTION_SETINFO, self.unique_id,
|
228 |
self.ext_params, metadata=text,
|
229 |
name=self.name, uuid=self.uuid) |
230 |
|
231 |
def GetUserspaceAccessUri(self, hypervisor): |
232 |
"""Generate KVM userspace URIs to be used as `-drive file` settings.
|
233 |
|
234 |
@see: L{base.BlockDev.GetUserspaceAccessUri}
|
235 |
|
236 |
"""
|
237 |
if not self.Attach(): |
238 |
base.ThrowError("Can't attach to ExtStorage device")
|
239 |
|
240 |
# If the provider supports userspace access, the attach script has
|
241 |
# returned a list of URIs prefixed with the corresponding hypervisor.
|
242 |
prefix = hypervisor.lower() + ":"
|
243 |
for uri in self.uris: |
244 |
if uri[:len(prefix)].lower() == prefix: |
245 |
return uri[len(prefix):] |
246 |
|
247 |
base.ThrowError("Userspace access is not supported by the '%s'"
|
248 |
" ExtStorage provider for the '%s' hypervisor"
|
249 |
% (self.driver, hypervisor))
|
250 |
|
251 |
def Snapshot(self, snapshot_name): |
252 |
"""Take a snapshot of the block device.
|
253 |
|
254 |
"""
|
255 |
# Call the External Storage's setinfo script,
|
256 |
# to set metadata for an existing Volume inside the External Storage
|
257 |
_ExtStorageAction(constants.ES_ACTION_SNAPSHOT, self.unique_id,
|
258 |
self.ext_params, snapshot_name=snapshot_name)
|
259 |
|
260 |
|
261 |
def _ExtStorageAction(action, unique_id, ext_params, |
262 |
size=None, grow=None, metadata=None, |
263 |
name=None, uuid=None, snapshot_name=None): |
264 |
"""Take an External Storage action.
|
265 |
|
266 |
Take an External Storage action concerning or affecting
|
267 |
a specific Volume inside the External Storage.
|
268 |
|
269 |
@type action: string
|
270 |
@param action: which action to perform. One of:
|
271 |
create / remove / grow / attach / detach
|
272 |
@type unique_id: tuple (driver, vol_name)
|
273 |
@param unique_id: a tuple containing the type of ExtStorage (driver)
|
274 |
and the Volume name
|
275 |
@type ext_params: dict
|
276 |
@param ext_params: ExtStorage parameters
|
277 |
@type size: integer
|
278 |
@param size: the size of the Volume in mebibytes
|
279 |
@type grow: integer
|
280 |
@param grow: the new size in mebibytes (after grow)
|
281 |
@type metadata: string
|
282 |
@param metadata: metadata info of the Volume, for use by the provider
|
283 |
@type name: string
|
284 |
@param name: name of the Volume (objects.Disk.name)
|
285 |
@type uuid: string
|
286 |
@param uuid: uuid of the Volume (objects.Disk.uuid)
|
287 |
@rtype: None or a block device path (during attach)
|
288 |
|
289 |
"""
|
290 |
driver, vol_name = unique_id |
291 |
|
292 |
# Create an External Storage instance of type `driver'
|
293 |
status, inst_es = ExtStorageFromDisk(driver) |
294 |
if not status: |
295 |
base.ThrowError("%s" % inst_es)
|
296 |
|
297 |
# Create the basic environment for the driver's scripts
|
298 |
create_env = _ExtStorageEnvironment(unique_id, ext_params, size, |
299 |
grow, metadata, name, uuid, |
300 |
snapshot_name) |
301 |
|
302 |
# Do not use log file for action `attach' as we need
|
303 |
# to get the output from RunResult
|
304 |
# TODO: find a way to have a log file for attach too
|
305 |
logfile = None
|
306 |
if action is not constants.ES_ACTION_ATTACH: |
307 |
logfile = _VolumeLogName(action, driver, vol_name) |
308 |
|
309 |
# Make sure the given action results in a valid script
|
310 |
if action not in constants.ES_SCRIPTS: |
311 |
base.ThrowError("Action '%s' doesn't result in a valid ExtStorage script" %
|
312 |
action) |
313 |
|
314 |
# Find out which external script to run according the given action
|
315 |
script_name = action + "_script"
|
316 |
script = getattr(inst_es, script_name)
|
317 |
|
318 |
# Run the external script
|
319 |
result = utils.RunCmd([script], env=create_env, |
320 |
cwd=inst_es.path, output=logfile,) |
321 |
if result.failed:
|
322 |
logging.error("External storage's %s command '%s' returned"
|
323 |
" error: %s, logfile: %s, output: %s",
|
324 |
action, result.cmd, result.fail_reason, |
325 |
logfile, result.output) |
326 |
|
327 |
# If logfile is 'None' (during attach), it breaks TailFile
|
328 |
# TODO: have a log file for attach too
|
329 |
if action is not constants.ES_ACTION_ATTACH: |
330 |
lines = [utils.SafeEncode(val) |
331 |
for val in utils.TailFile(logfile, lines=20)] |
332 |
else:
|
333 |
lines = result.output[-20:]
|
334 |
|
335 |
base.ThrowError("External storage's %s script failed (%s), last"
|
336 |
" lines of output:\n%s",
|
337 |
action, result.fail_reason, "\n".join(lines))
|
338 |
|
339 |
if action == constants.ES_ACTION_ATTACH:
|
340 |
return result.stdout
|
341 |
|
342 |
|
343 |
def ExtStorageFromDisk(name, base_dir=None): |
344 |
"""Create an ExtStorage instance from disk.
|
345 |
|
346 |
This function will return an ExtStorage instance
|
347 |
if the given name is a valid ExtStorage name.
|
348 |
|
349 |
@type base_dir: string
|
350 |
@keyword base_dir: Base directory containing ExtStorage installations.
|
351 |
Defaults to a search in all the ES_SEARCH_PATH dirs.
|
352 |
@rtype: tuple
|
353 |
@return: True and the ExtStorage instance if we find a valid one, or
|
354 |
False and the diagnose message on error
|
355 |
|
356 |
"""
|
357 |
if base_dir is None: |
358 |
es_base_dir = pathutils.ES_SEARCH_PATH |
359 |
else:
|
360 |
es_base_dir = [base_dir] |
361 |
|
362 |
es_dir = utils.FindFile(name, es_base_dir, os.path.isdir) |
363 |
|
364 |
if es_dir is None: |
365 |
return False, ("Directory for External Storage Provider %s not" |
366 |
" found in search path" % name)
|
367 |
|
368 |
# ES Files dictionary, we will populate it with the absolute path
|
369 |
# names; if the value is True, then it is a required file, otherwise
|
370 |
# an optional one
|
371 |
es_files = dict.fromkeys(constants.ES_SCRIPTS, True) |
372 |
|
373 |
es_files[constants.ES_PARAMETERS_FILE] = True
|
374 |
|
375 |
for (filename, _) in es_files.items(): |
376 |
es_files[filename] = utils.PathJoin(es_dir, filename) |
377 |
|
378 |
try:
|
379 |
st = os.stat(es_files[filename]) |
380 |
except EnvironmentError, err: |
381 |
return False, ("File '%s' under path '%s' is missing (%s)" % |
382 |
(filename, es_dir, utils.ErrnoOrStr(err))) |
383 |
|
384 |
if not stat.S_ISREG(stat.S_IFMT(st.st_mode)): |
385 |
return False, ("File '%s' under path '%s' is not a regular file" % |
386 |
(filename, es_dir)) |
387 |
|
388 |
if filename in constants.ES_SCRIPTS: |
389 |
if stat.S_IMODE(st.st_mode) & stat.S_IXUSR != stat.S_IXUSR:
|
390 |
return False, ("File '%s' under path '%s' is not executable" % |
391 |
(filename, es_dir)) |
392 |
|
393 |
parameters = [] |
394 |
if constants.ES_PARAMETERS_FILE in es_files: |
395 |
parameters_file = es_files[constants.ES_PARAMETERS_FILE] |
396 |
try:
|
397 |
parameters = utils.ReadFile(parameters_file).splitlines() |
398 |
except EnvironmentError, err: |
399 |
return False, ("Error while reading the EXT parameters file at %s: %s" % |
400 |
(parameters_file, utils.ErrnoOrStr(err))) |
401 |
parameters = [v.split(None, 1) for v in parameters] |
402 |
|
403 |
es_obj = \ |
404 |
objects.ExtStorage(name=name, path=es_dir, |
405 |
create_script=es_files[constants.ES_SCRIPT_CREATE], |
406 |
remove_script=es_files[constants.ES_SCRIPT_REMOVE], |
407 |
grow_script=es_files[constants.ES_SCRIPT_GROW], |
408 |
attach_script=es_files[constants.ES_SCRIPT_ATTACH], |
409 |
detach_script=es_files[constants.ES_SCRIPT_DETACH], |
410 |
setinfo_script=es_files[constants.ES_SCRIPT_SETINFO], |
411 |
verify_script=es_files[constants.ES_SCRIPT_VERIFY], |
412 |
snapshot_script=es_files[constants.ES_SCRIPT_SNAPSHOT], |
413 |
supported_parameters=parameters) |
414 |
return True, es_obj |
415 |
|
416 |
|
417 |
def _ExtStorageEnvironment(unique_id, ext_params, |
418 |
size=None, grow=None, metadata=None, |
419 |
name=None, uuid=None, snapshot_name=None): |
420 |
"""Calculate the environment for an External Storage script.
|
421 |
|
422 |
@type unique_id: tuple (driver, vol_name)
|
423 |
@param unique_id: ExtStorage pool and name of the Volume
|
424 |
@type ext_params: dict
|
425 |
@param ext_params: the EXT parameters
|
426 |
@type size: string
|
427 |
@param size: size of the Volume (in mebibytes)
|
428 |
@type grow: string
|
429 |
@param grow: new size of Volume after grow (in mebibytes)
|
430 |
@type metadata: string
|
431 |
@param metadata: metadata info of the Volume
|
432 |
@type name: string
|
433 |
@param name: name of the Volume (objects.Disk.name)
|
434 |
@type uuid: string
|
435 |
@param uuid: uuid of the Volume (objects.Disk.uuid)
|
436 |
@rtype: dict
|
437 |
@return: dict of environment variables
|
438 |
|
439 |
"""
|
440 |
vol_name = unique_id[1]
|
441 |
|
442 |
result = {} |
443 |
result["VOL_NAME"] = vol_name
|
444 |
|
445 |
# EXT params
|
446 |
for pname, pvalue in ext_params.items(): |
447 |
result["EXTP_%s" % pname.upper()] = str(pvalue) |
448 |
|
449 |
if size is not None: |
450 |
result["VOL_SIZE"] = size
|
451 |
|
452 |
if grow is not None: |
453 |
result["VOL_NEW_SIZE"] = grow
|
454 |
|
455 |
if metadata is not None: |
456 |
result["VOL_METADATA"] = metadata
|
457 |
|
458 |
if name is not None: |
459 |
result["VOL_CNAME"] = name
|
460 |
|
461 |
if uuid is not None: |
462 |
result["VOL_UUID"] = uuid
|
463 |
|
464 |
if snapshot_name is not None: |
465 |
result["VOL_SNAPSHOT_NAME"] = snapshot_name
|
466 |
|
467 |
return result
|
468 |
|
469 |
|
470 |
def _VolumeLogName(kind, es_name, volume): |
471 |
"""Compute the ExtStorage log filename for a given Volume and operation.
|
472 |
|
473 |
@type kind: string
|
474 |
@param kind: the operation type (e.g. create, remove etc.)
|
475 |
@type es_name: string
|
476 |
@param es_name: the ExtStorage name
|
477 |
@type volume: string
|
478 |
@param volume: the name of the Volume inside the External Storage
|
479 |
|
480 |
"""
|
481 |
# Check if the extstorage log dir is a valid dir
|
482 |
if not os.path.isdir(pathutils.LOG_ES_DIR): |
483 |
base.ThrowError("Cannot find log directory: %s", pathutils.LOG_ES_DIR)
|
484 |
|
485 |
# TODO: Use tempfile.mkstemp to create unique filename
|
486 |
basename = ("%s-%s-%s-%s.log" %
|
487 |
(kind, es_name, volume, utils.TimestampForFilename())) |
488 |
return utils.PathJoin(pathutils.LOG_ES_DIR, basename)
|