1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31 """ExtStorage Interface related functionality
32
33 """
34
35 import re
36 import stat
37 import os
38 import logging
39
40 from ganeti import utils
41 from ganeti import errors
42 from ganeti import constants
43 from ganeti import objects
44 from ganeti import pathutils
45 from ganeti.storage import base
49 """A block device provided by an ExtStorage Provider.
50
51 This class implements the External Storage Interface, which means
52 handling of the externally provided block devices.
53
54 """
55 - def __init__(self, unique_id, children, size, params, dyn_params, *args):
56 """Attaches to an extstorage block device.
57
58 """
59 super(ExtStorageDevice, self).__init__(unique_id, children, size, params,
60 dyn_params, *args)
61 (self.name, self.uuid) = args
62
63 if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
64 raise ValueError("Invalid configuration data %s" % str(unique_id))
65
66 self.driver, self.vol_name = unique_id
67 self.ext_params = params
68
69 self.major = self.minor = None
70 self.uris = []
71 self.Attach()
72
73 @classmethod
74 - def Create(cls, unique_id, children, size, spindles, params, excl_stor,
75 dyn_params, *args):
76 """Create a new extstorage device.
77
78 Provision a new volume using an extstorage provider, which will
79 then be mapped to a block device.
80
81 """
82 (name, uuid) = args
83
84 if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
85 raise errors.ProgrammerError("Invalid configuration data %s" %
86 str(unique_id))
87 if excl_stor:
88 raise errors.ProgrammerError("extstorage device requested with"
89 " exclusive_storage")
90
91
92
93 _ExtStorageAction(constants.ES_ACTION_CREATE, unique_id,
94 params, size=size, name=name, uuid=uuid)
95
96 return ExtStorageDevice(unique_id, children, size, params, dyn_params,
97 *args)
98
114
116 """Rename this device.
117
118 """
119 pass
120
122 """Attach to an existing extstorage device.
123
124 This method maps the extstorage volume that matches our name with
125 a corresponding block device and then attaches to this device.
126
127 """
128 self.attached = False
129
130
131
132 result = _ExtStorageAction(constants.ES_ACTION_ATTACH,
133 self.unique_id, self.ext_params,
134 name=self.name, uuid=self.uuid)
135
136
137
138
139
140
141 result = result.split("\n")
142 self.dev_path = result[0]
143 self.uris = result[1:]
144
145
146 try:
147 st = os.stat(self.dev_path)
148 except OSError, err:
149 logging.error("Error stat()'ing %s: %s", self.dev_path, str(err))
150 return False
151
152 if not stat.S_ISBLK(st.st_mode):
153 logging.error("%s is not a block device", self.dev_path)
154 return False
155
156 self.major = os.major(st.st_rdev)
157 self.minor = utils.osminor(st.st_rdev)
158 self.attached = True
159
160 return True
161
163 """Assemble the device.
164
165 """
166 pass
167
169 """Shutdown the device.
170
171 """
172 if not self.minor and not self.Attach():
173
174 return
175
176
177
178 _ExtStorageAction(constants.ES_ACTION_DETACH, self.unique_id,
179 self.ext_params, name=self.name, uuid=self.uuid)
180
181 self.minor = None
182 self.dev_path = None
183
184 - def Open(self, force=False):
185 """Make the device ready for I/O.
186
187 """
188 pass
189
191 """Notifies that the device will no longer be used for I/O.
192
193 """
194 pass
195
196 - def Grow(self, amount, dryrun, backingstore, excl_stor):
197 """Grow the Volume.
198
199 @type amount: integer
200 @param amount: the amount (in mebibytes) to grow with
201 @type dryrun: boolean
202 @param dryrun: whether to execute the operation in simulation mode
203 only, without actually increasing the size
204
205 """
206 if not backingstore:
207 return
208 if not self.Attach():
209 base.ThrowError("Can't attach to extstorage device during Grow()")
210
211 if dryrun:
212
213 return
214
215 new_size = self.size + amount
216
217
218
219 _ExtStorageAction(constants.ES_ACTION_GROW, self.unique_id,
220 self.ext_params, size=self.size, grow=new_size,
221 name=self.name, uuid=self.uuid)
222
239
241 """Generate KVM userspace URIs to be used as `-drive file` settings.
242
243 @see: L{base.BlockDev.GetUserspaceAccessUri}
244
245 """
246 if not self.Attach():
247 base.ThrowError("Can't attach to ExtStorage device")
248
249
250
251 prefix = hypervisor.lower() + ":"
252 for uri in self.uris:
253 if uri[:len(prefix)].lower() == prefix:
254 return uri[len(prefix):]
255
256 base.ThrowError("Userspace access is not supported by the '%s'"
257 " ExtStorage provider for the '%s' hypervisor"
258 % (self.driver, hypervisor))
259
260 - def Snapshot(self, snap_name=None, snap_size=None):
261 """Take a snapshot of the block device.
262
263 """
264 provider, vol_name = self.unique_id
265 if not snap_name:
266 snap_name = vol_name + ".snap"
267 if not snap_size:
268 snap_size = self.size
269
270 _ExtStorageAction(constants.ES_ACTION_SNAPSHOT, self.unique_id,
271 self.ext_params, snap_name=snap_name, snap_size=snap_size)
272
273 return (provider, snap_name)
274
275
276 -def _ExtStorageAction(action, unique_id, ext_params,
277 size=None, grow=None, metadata=None,
278 name=None, uuid=None,
279 snap_name=None, snap_size=None):
280 """Take an External Storage action.
281
282 Take an External Storage action concerning or affecting
283 a specific Volume inside the External Storage.
284
285 @type action: string
286 @param action: which action to perform. One of:
287 create / remove / grow / attach / detach / snapshot
288 @type unique_id: tuple (driver, vol_name)
289 @param unique_id: a tuple containing the type of ExtStorage (driver)
290 and the Volume name
291 @type ext_params: dict
292 @param ext_params: ExtStorage parameters
293 @type size: integer
294 @param size: the size of the Volume in mebibytes
295 @type grow: integer
296 @param grow: the new size in mebibytes (after grow)
297 @type metadata: string
298 @param metadata: metadata info of the Volume, for use by the provider
299 @type name: string
300 @param name: name of the Volume (objects.Disk.name)
301 @type uuid: string
302 @type snap_size: integer
303 @param snap_size: the size of the snapshot
304 @type snap_name: string
305 @param snap_name: the name of the snapshot
306 @param uuid: uuid of the Volume (objects.Disk.uuid)
307 @rtype: None or a block device path (during attach)
308
309 """
310 driver, vol_name = unique_id
311
312
313 status, inst_es = ExtStorageFromDisk(driver)
314 if not status:
315 base.ThrowError("%s" % inst_es)
316
317
318 create_env = _ExtStorageEnvironment(unique_id, ext_params, size,
319 grow, metadata, name, uuid,
320 snap_name, snap_size)
321
322
323
324
325 logfile = None
326 if action is not constants.ES_ACTION_ATTACH:
327 logfile = _VolumeLogName(action, driver, vol_name)
328
329
330 if action not in constants.ES_SCRIPTS:
331 base.ThrowError("Action '%s' doesn't result in a valid ExtStorage script" %
332 action)
333
334
335 try:
336 _CheckExtStorageFile(inst_es.path, action)
337 except errors.BlockDeviceError:
338 base.ThrowError("Action '%s' is not supported by provider '%s'" %
339 (action, driver))
340
341
342 script_name = action + "_script"
343 script = getattr(inst_es, script_name)
344
345
346
347 result = utils.RunCmd([script], env=create_env,
348 cwd=inst_es.path, output=logfile,)
349 if result.failed:
350 logging.error("External storage's %s command '%s' returned"
351 " error: %s, logfile: %s, output: %s",
352 action, result.cmd, result.fail_reason,
353 logfile, result.output)
354
355
356
357 if action is not constants.ES_ACTION_ATTACH:
358 lines = [utils.SafeEncode(val)
359 for val in utils.TailFile(logfile, lines=20)]
360 else:
361 lines = result.output.splitlines()[-20:]
362
363 base.ThrowError("External storage's %s script failed (%s), last"
364 " lines of output:\n%s",
365 action, result.fail_reason, "\n".join(lines))
366
367 if action == constants.ES_ACTION_ATTACH:
368 return result.stdout
369
372 """Check prereqs for an ExtStorage file.
373
374 Check if file exists, if it is a regular file and in case it is
375 one of extstorage scripts if it is executable.
376
377 @type base_dir: string
378 @param base_dir: Base directory containing ExtStorage installations.
379 @type filename: string
380 @param filename: The basename of the ExtStorage file.
381
382 @raises BlockDeviceError: In case prereqs are not met.
383
384 """
385
386 file_path = utils.PathJoin(base_dir, filename)
387 try:
388 st = os.stat(file_path)
389 except EnvironmentError, err:
390 base.ThrowError("File '%s' under path '%s' is missing (%s)" %
391 (filename, base_dir, utils.ErrnoOrStr(err)))
392
393 if not stat.S_ISREG(stat.S_IFMT(st.st_mode)):
394 base.ThrowError("File '%s' under path '%s' is not a regular file" %
395 (filename, base_dir))
396
397 if filename in constants.ES_SCRIPTS:
398 if stat.S_IMODE(st.st_mode) & stat.S_IXUSR != stat.S_IXUSR:
399 base.ThrowError("File '%s' under path '%s' is not executable" %
400 (filename, base_dir))
401
404 """Create an ExtStorage instance from disk.
405
406 This function will return an ExtStorage instance
407 if the given name is a valid ExtStorage name.
408
409 @type base_dir: string
410 @keyword base_dir: Base directory containing ExtStorage installations.
411 Defaults to a search in all the ES_SEARCH_PATH dirs.
412 @rtype: tuple
413 @return: True and the ExtStorage instance if we find a valid one, or
414 False and the diagnose message on error
415
416 """
417 if base_dir is None:
418 es_base_dir = pathutils.ES_SEARCH_PATH
419 else:
420 es_base_dir = [base_dir]
421
422 es_dir = utils.FindFile(name, es_base_dir, os.path.isdir)
423
424 if es_dir is None:
425 return False, ("Directory for External Storage Provider %s not"
426 " found in search path" % name)
427
428
429
430
431 es_files = dict.fromkeys(constants.ES_SCRIPTS, True)
432
433
434 es_files[constants.ES_SCRIPT_SNAPSHOT] = False
435
436 es_files[constants.ES_PARAMETERS_FILE] = True
437
438 for (filename, required) in es_files.items():
439 es_files[filename] = utils.PathJoin(es_dir, filename)
440 try:
441 _CheckExtStorageFile(es_dir, filename)
442 except errors.BlockDeviceError, err:
443 if required:
444 return False, str(err)
445
446 parameters = []
447 if constants.ES_PARAMETERS_FILE in es_files:
448 parameters_file = es_files[constants.ES_PARAMETERS_FILE]
449 try:
450 parameters = utils.ReadFile(parameters_file).splitlines()
451 except EnvironmentError, err:
452 return False, ("Error while reading the EXT parameters file at %s: %s" %
453 (parameters_file, utils.ErrnoOrStr(err)))
454 parameters = [v.split(None, 1) for v in parameters]
455
456 es_obj = \
457 objects.ExtStorage(name=name, path=es_dir,
458 create_script=es_files[constants.ES_SCRIPT_CREATE],
459 remove_script=es_files[constants.ES_SCRIPT_REMOVE],
460 grow_script=es_files[constants.ES_SCRIPT_GROW],
461 attach_script=es_files[constants.ES_SCRIPT_ATTACH],
462 detach_script=es_files[constants.ES_SCRIPT_DETACH],
463 setinfo_script=es_files[constants.ES_SCRIPT_SETINFO],
464 verify_script=es_files[constants.ES_SCRIPT_VERIFY],
465 snapshot_script=es_files[constants.ES_SCRIPT_SNAPSHOT],
466 supported_parameters=parameters)
467 return True, es_obj
468
469
470 -def _ExtStorageEnvironment(unique_id, ext_params,
471 size=None, grow=None, metadata=None,
472 name=None, uuid=None,
473 snap_name=None, snap_size=None):
474 """Calculate the environment for an External Storage script.
475
476 @type unique_id: tuple (driver, vol_name)
477 @param unique_id: ExtStorage pool and name of the Volume
478 @type ext_params: dict
479 @param ext_params: the EXT parameters
480 @type size: integer
481 @param size: size of the Volume (in mebibytes)
482 @type grow: integer
483 @param grow: new size of Volume after grow (in mebibytes)
484 @type metadata: string
485 @param metadata: metadata info of the Volume
486 @type name: string
487 @param name: name of the Volume (objects.Disk.name)
488 @type uuid: string
489 @param uuid: uuid of the Volume (objects.Disk.uuid)
490 @type snap_size: integer
491 @param snap_size: the size of the snapshot
492 @type snap_name: string
493 @param snap_name: the name of the snapshot
494 @rtype: dict
495 @return: dict of environment variables
496
497 """
498 vol_name = unique_id[1]
499
500 result = {}
501 result["VOL_NAME"] = vol_name
502
503
504 for pname, pvalue in ext_params.items():
505 result["EXTP_%s" % pname.upper()] = str(pvalue)
506
507 if size is not None:
508 result["VOL_SIZE"] = str(size)
509
510 if grow is not None:
511 result["VOL_NEW_SIZE"] = str(grow)
512
513 if metadata is not None:
514 result["VOL_METADATA"] = metadata
515
516 if name is not None:
517 result["VOL_CNAME"] = name
518
519 if uuid is not None:
520 result["VOL_UUID"] = uuid
521
522 if snap_name is not None:
523 result["VOL_SNAPSHOT_NAME"] = snap_name
524
525 if snap_size is not None:
526 result["VOL_SNAPSHOT_SIZE"] = str(snap_size)
527
528 return result
529
532 """Compute the ExtStorage log filename for a given Volume and operation.
533
534 @type kind: string
535 @param kind: the operation type (e.g. create, remove etc.)
536 @type es_name: string
537 @param es_name: the ExtStorage name
538 @type volume: string
539 @param volume: the name of the Volume inside the External Storage
540
541 """
542
543 if not os.path.isdir(pathutils.LOG_ES_DIR):
544 base.ThrowError("Cannot find log directory: %s", pathutils.LOG_ES_DIR)
545
546
547 basename = ("%s-%s-%s-%s.log" %
548 (kind, es_name, volume, utils.TimestampForFilename()))
549 return utils.PathJoin(pathutils.LOG_ES_DIR, basename)
550