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 if not self.dev_path:
146 logging.info("A local block device is not available")
147 self.dev_path = None
148 if not self.uris:
149 logging.error("Neither a block device nor a userspace URI is available")
150 return False
151
152 self.attached = True
153 return True
154
155
156 try:
157 st = os.stat(self.dev_path)
158 except OSError, err:
159 logging.error("Error stat()'ing %s: %s", self.dev_path, str(err))
160 return False
161
162 if not stat.S_ISBLK(st.st_mode):
163 logging.error("%s is not a block device", self.dev_path)
164 return False
165
166 self.major = os.major(st.st_rdev)
167 self.minor = utils.osminor(st.st_rdev)
168 self.attached = True
169
170 return True
171
173 """Assemble the device.
174
175 """
176 pass
177
179 """Shutdown the device.
180
181 """
182 if not self.minor and not self.Attach():
183
184 return
185
186
187
188 _ExtStorageAction(constants.ES_ACTION_DETACH, self.unique_id,
189 self.ext_params, name=self.name, uuid=self.uuid)
190
191 self.minor = None
192 self.dev_path = None
193
194 - def Open(self, force=False, exclusive=True):
202
210
211 - def Grow(self, amount, dryrun, backingstore, excl_stor):
212 """Grow the Volume.
213
214 @type amount: integer
215 @param amount: the amount (in mebibytes) to grow with
216 @type dryrun: boolean
217 @param dryrun: whether to execute the operation in simulation mode
218 only, without actually increasing the size
219
220 """
221 if not backingstore:
222 return
223 if not self.Attach():
224 base.ThrowError("Can't attach to extstorage device during Grow()")
225
226 if dryrun:
227
228 return
229
230 new_size = self.size + amount
231
232
233
234 _ExtStorageAction(constants.ES_ACTION_GROW, self.unique_id,
235 self.ext_params, size=self.size, grow=new_size,
236 name=self.name, uuid=self.uuid)
237
254
256 """Generate KVM userspace URIs to be used as `-drive file` settings.
257
258 @see: L{base.BlockDev.GetUserspaceAccessUri}
259
260 """
261 if not self.Attach():
262 base.ThrowError("Can't attach to ExtStorage device")
263
264
265
266 prefix = hypervisor.lower() + ":"
267 for uri in self.uris:
268 if uri[:len(prefix)].lower() == prefix:
269 return uri[len(prefix):]
270
271 base.ThrowError("Userspace access is not supported by the '%s'"
272 " ExtStorage provider for the '%s' hypervisor"
273 % (self.driver, hypervisor))
274
275 - def Snapshot(self, snap_name=None, snap_size=None):
276 """Take a snapshot of the block device.
277
278 """
279 provider, vol_name = self.unique_id
280 if not snap_name:
281 snap_name = vol_name + ".snap"
282 if not snap_size:
283 snap_size = self.size
284
285 _ExtStorageAction(constants.ES_ACTION_SNAPSHOT, self.unique_id,
286 self.ext_params, snap_name=snap_name, snap_size=snap_size)
287
288 return (provider, snap_name)
289
290
291 -def _ExtStorageAction(action, unique_id, ext_params,
292 size=None, grow=None, metadata=None,
293 name=None, uuid=None,
294 snap_name=None, snap_size=None,
295 exclusive=None):
296 """Take an External Storage action.
297
298 Take an External Storage action concerning or affecting
299 a specific Volume inside the External Storage.
300
301 @type action: string
302 @param action: which action to perform. One of:
303 create / remove / grow / attach / detach / snapshot
304 @type unique_id: tuple (driver, vol_name)
305 @param unique_id: a tuple containing the type of ExtStorage (driver)
306 and the Volume name
307 @type ext_params: dict
308 @param ext_params: ExtStorage parameters
309 @type size: integer
310 @param size: the size of the Volume in mebibytes
311 @type grow: integer
312 @param grow: the new size in mebibytes (after grow)
313 @type metadata: string
314 @param metadata: metadata info of the Volume, for use by the provider
315 @type name: string
316 @param name: name of the Volume (objects.Disk.name)
317 @type uuid: string
318 @type snap_size: integer
319 @param snap_size: the size of the snapshot
320 @type snap_name: string
321 @param snap_name: the name of the snapshot
322 @type exclusive: boolean
323 @param exclusive: Whether the Volume will be opened exclusively or not
324 @param uuid: uuid of the Volume (objects.Disk.uuid)
325 @rtype: None or a block device path (during attach)
326
327 """
328 driver, vol_name = unique_id
329
330
331 status, inst_es = ExtStorageFromDisk(driver)
332 if not status:
333 base.ThrowError("%s" % inst_es)
334
335
336 create_env = _ExtStorageEnvironment(unique_id, ext_params, size,
337 grow, metadata, name, uuid,
338 snap_name, snap_size,
339 exclusive)
340
341
342
343
344 logfile = None
345 if action is not constants.ES_ACTION_ATTACH:
346 logfile = _VolumeLogName(action, driver, vol_name)
347
348
349 if action not in constants.ES_SCRIPTS:
350 base.ThrowError("Action '%s' doesn't result in a valid ExtStorage script" %
351 action)
352
353
354 script_name = action + "_script"
355 script = getattr(inst_es, script_name)
356
357
358 if not script:
359 logging.info("Optional action '%s' is not supported by provider '%s',"
360 " skipping", action, driver)
361 return
362
363
364
365 result = utils.RunCmd([script], env=create_env,
366 cwd=inst_es.path, output=logfile,)
367 if result.failed:
368 logging.error("External storage's %s command '%s' returned"
369 " error: %s, logfile: %s, output: %s",
370 action, result.cmd, result.fail_reason,
371 logfile, result.output)
372
373
374
375 if action is not constants.ES_ACTION_ATTACH:
376 lines = [utils.SafeEncode(val)
377 for val in utils.TailFile(logfile, lines=20)]
378 else:
379 lines = result.output.splitlines()[-20:]
380
381 base.ThrowError("External storage's %s script failed (%s), last"
382 " lines of output:\n%s",
383 action, result.fail_reason, "\n".join(lines))
384
385 if action == constants.ES_ACTION_ATTACH:
386 return result.stdout
387
390 """Check prereqs for an ExtStorage file.
391
392 Check if file exists, if it is a regular file and in case it is
393 one of extstorage scripts if it is executable.
394
395 @type base_dir: string
396 @param base_dir: Base directory containing ExtStorage installations.
397 @type filename: string
398 @param filename: The basename of the ExtStorage file.
399 @type required: bool
400 @param required: Whether the file is required or not.
401
402 @rtype: String
403 @return: The file path if the file is found and is valid,
404 None if the file is not found and not required.
405
406 @raises BlockDeviceError: In case prereqs are not met
407 (found and not valid/executable, not found and required)
408
409 """
410
411 file_path = utils.PathJoin(base_dir, filename)
412 try:
413 st = os.stat(file_path)
414 except EnvironmentError, err:
415 if not required:
416 logging.info("Optional file '%s' under path '%s' is missing",
417 filename, base_dir)
418 return None
419
420 base.ThrowError("File '%s' under path '%s' is missing (%s)" %
421 (filename, base_dir, utils.ErrnoOrStr(err)))
422
423 if not stat.S_ISREG(stat.S_IFMT(st.st_mode)):
424 base.ThrowError("File '%s' under path '%s' is not a regular file" %
425 (filename, base_dir))
426
427 if filename in constants.ES_SCRIPTS:
428 if stat.S_IMODE(st.st_mode) & stat.S_IXUSR != stat.S_IXUSR:
429 base.ThrowError("File '%s' under path '%s' is not executable" %
430 (filename, base_dir))
431
432 return file_path
433
436 """Create an ExtStorage instance from disk.
437
438 This function will return an ExtStorage instance
439 if the given name is a valid ExtStorage name.
440
441 @type base_dir: string
442 @keyword base_dir: Base directory containing ExtStorage installations.
443 Defaults to a search in all the ES_SEARCH_PATH dirs.
444 @rtype: tuple
445 @return: True and the ExtStorage instance if we find a valid one, or
446 False and the diagnose message on error
447
448 """
449 if base_dir is None:
450 es_base_dir = pathutils.ES_SEARCH_PATH
451 else:
452 es_base_dir = [base_dir]
453
454 es_dir = utils.FindFile(name, es_base_dir, os.path.isdir)
455
456 if es_dir is None:
457 return False, ("Directory for External Storage Provider %s not"
458 " found in search path" % name)
459
460
461
462
463 es_files = dict.fromkeys(constants.ES_SCRIPTS, True)
464
465
466
467 es_files[constants.ES_SCRIPT_SNAPSHOT] = False
468 es_files[constants.ES_SCRIPT_OPEN] = False
469 es_files[constants.ES_SCRIPT_CLOSE] = False
470
471 es_files[constants.ES_PARAMETERS_FILE] = True
472
473 for (filename, required) in es_files.items():
474 try:
475
476
477
478 es_files[filename] = _CheckExtStorageFile(es_dir, filename, required)
479 except errors.BlockDeviceError, err:
480 return False, str(err)
481
482 parameters = []
483 if constants.ES_PARAMETERS_FILE in es_files:
484 parameters_file = es_files[constants.ES_PARAMETERS_FILE]
485 try:
486 parameters = utils.ReadFile(parameters_file).splitlines()
487 except EnvironmentError, err:
488 return False, ("Error while reading the EXT parameters file at %s: %s" %
489 (parameters_file, utils.ErrnoOrStr(err)))
490 parameters = [v.split(None, 1) for v in parameters]
491
492 es_obj = \
493 objects.ExtStorage(name=name, path=es_dir,
494 create_script=es_files[constants.ES_SCRIPT_CREATE],
495 remove_script=es_files[constants.ES_SCRIPT_REMOVE],
496 grow_script=es_files[constants.ES_SCRIPT_GROW],
497 attach_script=es_files[constants.ES_SCRIPT_ATTACH],
498 detach_script=es_files[constants.ES_SCRIPT_DETACH],
499 setinfo_script=es_files[constants.ES_SCRIPT_SETINFO],
500 verify_script=es_files[constants.ES_SCRIPT_VERIFY],
501 snapshot_script=es_files[constants.ES_SCRIPT_SNAPSHOT],
502 open_script=es_files[constants.ES_SCRIPT_OPEN],
503 close_script=es_files[constants.ES_SCRIPT_CLOSE],
504 supported_parameters=parameters)
505 return True, es_obj
506
507
508 -def _ExtStorageEnvironment(unique_id, ext_params,
509 size=None, grow=None, metadata=None,
510 name=None, uuid=None,
511 snap_name=None, snap_size=None,
512 exclusive=None):
513 """Calculate the environment for an External Storage script.
514
515 @type unique_id: tuple (driver, vol_name)
516 @param unique_id: ExtStorage pool and name of the Volume
517 @type ext_params: dict
518 @param ext_params: the EXT parameters
519 @type size: integer
520 @param size: size of the Volume (in mebibytes)
521 @type grow: integer
522 @param grow: new size of Volume after grow (in mebibytes)
523 @type metadata: string
524 @param metadata: metadata info of the Volume
525 @type name: string
526 @param name: name of the Volume (objects.Disk.name)
527 @type uuid: string
528 @param uuid: uuid of the Volume (objects.Disk.uuid)
529 @type snap_size: integer
530 @param snap_size: the size of the snapshot
531 @type snap_name: string
532 @param snap_name: the name of the snapshot
533 @type exclusive: boolean
534 @param exclusive: Whether the Volume will be opened exclusively or not
535 @rtype: dict
536 @return: dict of environment variables
537
538 """
539 vol_name = unique_id[1]
540
541 result = {}
542 result["VOL_NAME"] = vol_name
543
544
545 for pname, pvalue in ext_params.items():
546 result["EXTP_%s" % pname.upper()] = str(pvalue)
547
548 if size is not None:
549 result["VOL_SIZE"] = str(size)
550
551 if grow is not None:
552 result["VOL_NEW_SIZE"] = str(grow)
553
554 if metadata is not None:
555 result["VOL_METADATA"] = metadata
556
557 if name is not None:
558 result["VOL_CNAME"] = name
559
560 if uuid is not None:
561 result["VOL_UUID"] = uuid
562
563 if snap_name is not None:
564 result["VOL_SNAPSHOT_NAME"] = snap_name
565
566 if snap_size is not None:
567 result["VOL_SNAPSHOT_SIZE"] = str(snap_size)
568
569 if exclusive is not None:
570 result["VOL_OPEN_EXCLUSIVE"] = str(exclusive)
571
572 return result
573
576 """Compute the ExtStorage log filename for a given Volume and operation.
577
578 @type kind: string
579 @param kind: the operation type (e.g. create, remove etc.)
580 @type es_name: string
581 @param es_name: the ExtStorage name
582 @type volume: string
583 @param volume: the name of the Volume inside the External Storage
584
585 """
586
587 if not os.path.isdir(pathutils.LOG_ES_DIR):
588 base.ThrowError("Cannot find log directory: %s", pathutils.LOG_ES_DIR)
589
590
591 basename = ("%s-%s-%s-%s.log" %
592 (kind, es_name, volume, utils.TimestampForFilename()))
593 return utils.PathJoin(pathutils.LOG_ES_DIR, basename)
594