1
# vim: tabstop=4 shiftwidth=4 softtabstop=4
2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
4
# Copyright 2010 United States Government as represented by the
5
# Administrator of the National Aeronautics and Space Administration.
6
# Copyright 2011 Justin Santa Barbara
9
# Licensed under the Apache License, Version 2.0 (the "License"); you may
10
# not use this file except in compliance with the License. You may obtain
11
# a copy of the License at
13
# http://www.apache.org/licenses/LICENSE-2.0
15
# Unless required by applicable law or agreed to in writing, software
16
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
17
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
18
# License for the specific language governing permissions and limitations
21
"""Utilities and helper functions."""
45
from xml.sax import saxutils
47
from eventlet import event
48
from eventlet import greenthread
49
from eventlet.green import subprocess
51
from cinder.common import deprecated
52
from cinder import exception
53
from cinder import flags
54
from cinder.openstack.common import log as logging
55
from cinder.openstack.common import excutils
56
from cinder.openstack.common import importutils
57
from cinder.openstack.common import timeutils
60
LOG = logging.getLogger(__name__)
61
ISO_TIME_FORMAT = "%Y-%m-%dT%H:%M:%S"
62
PERFECT_TIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%f"
66
def find_config(config_path):
67
"""Find a configuration file using the given hint.
69
:param config_path: Full or relative path to the config.
70
:returns: Full path of the config, if it exists.
71
:raises: `cinder.exception.ConfigNotFound`
74
possible_locations = [
76
os.path.join(FLAGS.state_path, "etc", "cinder", config_path),
77
os.path.join(FLAGS.state_path, "etc", config_path),
78
os.path.join(FLAGS.state_path, config_path),
79
"/etc/cinder/%s" % config_path,
82
for path in possible_locations:
83
if os.path.exists(path):
84
return os.path.abspath(path)
86
raise exception.ConfigNotFound(path=os.path.abspath(config_path))
89
def fetchfile(url, target):
90
LOG.debug(_('Fetching %s') % url)
91
execute('curl', '--fail', url, '-o', target)
94
def _subprocess_setup():
95
# Python installs a SIGPIPE handler by default. This is usually not what
96
# non-Python subprocesses expect.
97
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
100
def execute(*cmd, **kwargs):
101
"""Helper method to execute command with optional retry.
103
If you add a run_as_root=True command, don't forget to add the
104
corresponding filter to etc/cinder/rootwrap.d !
106
:param cmd: Passed to subprocess.Popen.
107
:param process_input: Send to opened process.
108
:param check_exit_code: Single bool, int, or list of allowed exit
109
codes. Defaults to [0]. Raise
110
exception.ProcessExecutionError unless
111
program exits with one of these code.
112
:param delay_on_retry: True | False. Defaults to True. If set to
113
True, wait a short amount of time
115
:param attempts: How many times to retry cmd.
116
:param run_as_root: True | False. Defaults to False. If set to True,
117
the command is prefixed by the command specified
118
in the root_helper FLAG.
120
:raises exception.Error: on receiving unknown arguments
121
:raises exception.ProcessExecutionError:
123
:returns: a tuple, (stdout, stderr) from the spawned process, or None if
127
process_input = kwargs.pop('process_input', None)
128
check_exit_code = kwargs.pop('check_exit_code', [0])
129
ignore_exit_code = False
130
if isinstance(check_exit_code, bool):
131
ignore_exit_code = not check_exit_code
132
check_exit_code = [0]
133
elif isinstance(check_exit_code, int):
134
check_exit_code = [check_exit_code]
135
delay_on_retry = kwargs.pop('delay_on_retry', True)
136
attempts = kwargs.pop('attempts', 1)
137
run_as_root = kwargs.pop('run_as_root', False)
138
shell = kwargs.pop('shell', False)
141
raise exception.Error(_('Got unknown keyword args '
142
'to utils.execute: %r') % kwargs)
146
if FLAGS.rootwrap_config is None or FLAGS.root_helper != 'sudo':
147
deprecated.warn(_('The root_helper option (which lets you specify '
148
'a root wrapper different from cinder-rootwrap, '
149
'and defaults to using sudo) is now deprecated. '
150
'You should use the rootwrap_config option '
153
if (FLAGS.rootwrap_config is not None):
154
cmd = ['sudo', 'cinder-rootwrap',
155
FLAGS.rootwrap_config] + list(cmd)
157
cmd = shlex.split(FLAGS.root_helper) + list(cmd)
163
LOG.debug(_('Running cmd (subprocess): %s'), ' '.join(cmd))
164
_PIPE = subprocess.PIPE # pylint: disable=E1101
165
obj = subprocess.Popen(cmd,
170
preexec_fn=_subprocess_setup,
173
if process_input is not None:
174
result = obj.communicate(process_input)
176
result = obj.communicate()
177
obj.stdin.close() # pylint: disable=E1101
178
_returncode = obj.returncode # pylint: disable=E1101
180
LOG.debug(_('Result was %s') % _returncode)
181
if not ignore_exit_code and _returncode not in check_exit_code:
182
(stdout, stderr) = result
183
raise exception.ProcessExecutionError(
184
exit_code=_returncode,
189
except exception.ProcessExecutionError:
193
LOG.debug(_('%r failed. Retrying.'), cmd)
195
greenthread.sleep(random.randint(20, 200) / 100.0)
197
# NOTE(termie): this appears to be necessary to let the subprocess
198
# call clean something up in between calls, without
199
# it two execute calls in a row hangs the second one
203
def trycmd(*args, **kwargs):
205
A wrapper around execute() to more easily handle warnings and errors.
207
Returns an (out, err) tuple of strings containing the output of
208
the command's stdout and stderr. If 'err' is not empty then the
209
command can be considered to have failed.
211
:discard_warnings True | False. Defaults to False. If set to True,
212
then for succeeding commands, stderr is cleared
215
discard_warnings = kwargs.pop('discard_warnings', False)
218
out, err = execute(*args, **kwargs)
220
except exception.ProcessExecutionError, exn:
221
out, err = '', str(exn)
225
if not failed and discard_warnings and err:
226
# Handle commands that output to stderr but otherwise succeed
233
def ssh_execute(ssh, cmd, process_input=None,
234
addl_env=None, check_exit_code=True):
235
LOG.debug(_('Running cmd (SSH): %s'), ' '.join(cmd))
237
raise exception.Error(_('Environment not supported over SSH'))
240
# This is (probably) fixable if we need it...
241
raise exception.Error(_('process_input not supported over SSH'))
243
stdin_stream, stdout_stream, stderr_stream = ssh.exec_command(cmd)
244
channel = stdout_stream.channel
246
#stdin.write('process_input would go here')
249
# NOTE(justinsb): This seems suspicious...
250
# ...other SSH clients have buffering issues with this approach
251
stdout = stdout_stream.read()
252
stderr = stderr_stream.read()
255
exit_status = channel.recv_exit_status()
257
# exit_status == -1 if no exit code was returned
258
if exit_status != -1:
259
LOG.debug(_('Result was %s') % exit_status)
260
if check_exit_code and exit_status != 0:
261
raise exception.ProcessExecutionError(exit_code=exit_status,
266
return (stdout, stderr)
271
return os.path.abspath(cinder.__file__).split('cinder/__init__.py')[0]
275
LOG.debug(_('debug in callback: %s'), arg)
279
def generate_uid(topic, size=8):
280
characters = '01234567890abcdefghijklmnopqrstuvwxyz'
281
choices = [random.choice(characters) for x in xrange(size)]
282
return '%s-%s' % (topic, ''.join(choices))
285
# Default symbols to use for passwords. Avoids visually confusing characters.
287
DEFAULT_PASSWORD_SYMBOLS = ('23456789', # Removed: 0,1
288
'ABCDEFGHJKLMNPQRSTUVWXYZ', # Removed: I, O
289
'abcdefghijkmnopqrstuvwxyz') # Removed: l
293
EASIER_PASSWORD_SYMBOLS = ('23456789', # Removed: 0, 1
294
'ABCDEFGHJKLMNPQRSTUVWXYZ') # Removed: I, O
297
def last_completed_audit_period(unit=None):
298
"""This method gives you the most recently *completed* audit period.
301
units: string, one of 'hour', 'day', 'month', 'year'
302
Periods normally begin at the beginning (UTC) of the
303
period unit (So a 'day' period begins at midnight UTC,
304
a 'month' unit on the 1st, a 'year' on Jan, 1)
305
unit string may be appended with an optional offset
306
like so: 'day@18' This will begin the period at 18:00
307
UTC. 'month@15' starts a monthly period on the 15th,
308
and year@3 begins a yearly one on March 1st.
311
returns: 2 tuple of datetimes (begin, end)
312
The begin timestamp of this audit period is the same as the
313
end of the previous."""
315
unit = FLAGS.instance_usage_audit_period
319
unit, offset = unit.split("@", 1)
322
rightnow = timeutils.utcnow()
323
if unit not in ('month', 'day', 'year', 'hour'):
324
raise ValueError('Time period must be hour, day, month or year')
328
end = datetime.datetime(day=offset,
329
month=rightnow.month,
333
if 1 >= rightnow.month:
335
month = 12 + (rightnow.month - 1)
337
month = rightnow.month - 1
338
end = datetime.datetime(day=offset,
344
month = 12 + (end.month - 1)
346
month = end.month - 1
347
begin = datetime.datetime(day=offset, month=month, year=year)
352
end = datetime.datetime(day=1, month=offset, year=rightnow.year)
354
end = datetime.datetime(day=1,
356
year=rightnow.year - 1)
357
begin = datetime.datetime(day=1,
359
year=rightnow.year - 2)
361
begin = datetime.datetime(day=1,
363
year=rightnow.year - 1)
366
end = datetime.datetime(hour=offset,
368
month=rightnow.month,
371
end = end - datetime.timedelta(days=1)
372
begin = end - datetime.timedelta(days=1)
375
end = rightnow.replace(minute=offset, second=0, microsecond=0)
377
end = end - datetime.timedelta(hours=1)
378
begin = end - datetime.timedelta(hours=1)
383
def generate_password(length=20, symbolgroups=DEFAULT_PASSWORD_SYMBOLS):
384
"""Generate a random password from the supplied symbol groups.
386
At least one symbol from each group will be included. Unpredictable
387
results if length is less than the number of symbol groups.
389
Believed to be reasonably secure (with a reasonable password length!)
392
r = random.SystemRandom()
394
# NOTE(jerdfelt): Some password policies require at least one character
395
# from each group of symbols, so start off with one random character
396
# from each symbol group
397
password = [r.choice(s) for s in symbolgroups]
398
# If length < len(symbolgroups), the leading characters will only
399
# be from the first length groups. Try our best to not be predictable
400
# by shuffling and then truncating.
402
password = password[:length]
403
length -= len(password)
405
# then fill with random characters from all symbol groups
406
symbols = ''.join(symbolgroups)
407
password.extend([r.choice(symbols) for _i in xrange(length)])
409
# finally shuffle to ensure first x characters aren't from a
413
return ''.join(password)
416
def last_octet(address):
417
return int(address.split('.')[-1])
420
def get_my_linklocal(interface):
422
if_str = execute('ip', '-f', 'inet6', '-o', 'addr', 'show', interface)
423
condition = '\s+inet6\s+([0-9a-f:]+)/\d+\s+scope\s+link'
424
links = [re.search(condition, x) for x in if_str[0].split('\n')]
425
address = [w.group(1) for w in links if w is not None]
426
if address[0] is not None:
429
raise exception.Error(_('Link Local address is not found.:%s')
431
except Exception as ex:
432
raise exception.Error(_("Couldn't get Link Local IP of %(interface)s"
433
" :%(ex)s") % locals())
436
def parse_mailmap(mailmap='.mailmap'):
438
if os.path.exists(mailmap):
439
fp = open(mailmap, 'r')
442
if not l.startswith('#') and ' ' in l:
443
canonical_email, alias = l.split(' ')
444
mapping[alias.lower()] = canonical_email.lower()
448
def str_dict_replace(s, mapping):
449
for s1, s2 in mapping.iteritems():
450
s = s.replace(s1, s2)
454
class LazyPluggable(object):
455
"""A pluggable backend loaded lazily based on some value."""
457
def __init__(self, pivot, **backends):
458
self.__backends = backends
460
self.__backend = None
462
def __get_backend(self):
463
if not self.__backend:
464
backend_name = FLAGS[self.__pivot]
465
if backend_name not in self.__backends:
466
raise exception.Error(_('Invalid backend: %s') % backend_name)
468
backend = self.__backends[backend_name]
469
if isinstance(backend, tuple):
471
fromlist = backend[1]
476
self.__backend = __import__(name, None, None, fromlist)
477
LOG.debug(_('backend %s'), self.__backend)
478
return self.__backend
480
def __getattr__(self, key):
481
backend = self.__get_backend()
482
return getattr(backend, key)
485
class LoopingCallDone(Exception):
486
"""Exception to break out and stop a LoopingCall.
488
The poll-function passed to LoopingCall can raise this exception to
489
break out of the loop normally. This is somewhat analogous to
492
An optional return-value can be included as the argument to the exception;
493
this return-value will be returned by LoopingCall.wait()
497
def __init__(self, retvalue=True):
498
""":param retvalue: Value that LoopingCall.wait() should return."""
499
self.retvalue = retvalue
502
class LoopingCall(object):
503
def __init__(self, f=None, *args, **kw):
507
self._running = False
509
def start(self, interval, initial_delay=None):
515
greenthread.sleep(initial_delay)
519
self.f(*self.args, **self.kw)
520
if not self._running:
522
greenthread.sleep(interval)
523
except LoopingCallDone, e:
525
done.send(e.retvalue)
527
LOG.exception(_('in looping call'))
528
done.send_exception(*sys.exc_info())
535
greenthread.spawn(_inner)
539
self._running = False
542
return self.done.wait()
545
def xhtml_escape(value):
546
"""Escapes a string so it is valid within XML or XHTML.
549
return saxutils.escape(value, {'"': '"', "'": '''})
553
"""Try to turn a string into utf-8 if possible.
555
Code is directly from the utf8 function in
556
http://github.com/facebook/tornado/blob/master/tornado/escape.py
559
if isinstance(value, unicode):
560
return value.encode('utf-8')
561
assert isinstance(value, str)
565
def delete_if_exists(pathname):
566
"""delete a file, but ignore file not found error"""
571
if e.errno == errno.ENOENT:
577
def get_from_path(items, path):
578
"""Returns a list of items matching the specified path.
580
Takes an XPath-like expression e.g. prop1/prop2/prop3, and for each item
581
in items, looks up items[prop1][prop2][prop3]. Like XPath, if any of the
582
intermediate results are lists it will treat each list item individually.
583
A 'None' in items or any child expressions will be ignored, this function
584
will not throw because of None (anywhere) in items. The returned list
585
will contain no None values.
589
raise exception.Error('Invalid mini_xpath')
591
(first_token, sep, remainder) = path.partition('/')
593
if first_token == '':
594
raise exception.Error('Invalid mini_xpath')
601
if not isinstance(items, list):
602
# Wrap single objects in a list
608
get_method = getattr(item, 'get', None)
609
if get_method is None:
611
child = get_method(first_token)
614
if isinstance(child, list):
615
# Flatten intermediate lists
619
results.append(child)
625
return get_from_path(results, remainder)
628
def flatten_dict(dict_, flattened=None):
629
"""Recursively flatten a nested dictionary."""
630
flattened = flattened or {}
631
for key, value in dict_.iteritems():
632
if hasattr(value, 'iteritems'):
633
flatten_dict(value, flattened)
635
flattened[key] = value
639
def partition_dict(dict_, keys):
640
"""Return two dicts, one with `keys` the other with everything else."""
643
for key, value in dict_.iteritems():
645
intersection[key] = value
647
difference[key] = value
648
return intersection, difference
651
def map_dict_keys(dict_, key_map):
652
"""Return a dict in which the dictionaries keys are mapped to new keys."""
654
for key, value in dict_.iteritems():
655
mapped_key = key_map[key] if key in key_map else key
656
mapped[mapped_key] = value
660
def subset_dict(dict_, keys):
661
"""Return a dict that only contains a subset of keys."""
662
subset = partition_dict(dict_, keys)[0]
666
def check_isinstance(obj, cls):
667
"""Checks that obj is of type cls, and lets PyLint infer types."""
668
if isinstance(obj, cls):
670
raise Exception(_('Expected object of type: %s') % (str(cls)))
671
# TODO(justinsb): Can we make this better??
672
return cls() # Ugly PyLint hack
679
def is_uuid_like(val):
680
"""For our purposes, a UUID is a string in canonical form:
682
aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa
687
except (TypeError, ValueError, AttributeError):
691
def bool_from_str(val):
692
"""Convert a string representation of a bool into a bool value"""
697
return True if int(val) else False
699
return val.lower() == 'true'
702
def is_valid_boolstr(val):
703
"""Check if the provided string is a valid bool string or not. """
704
val = str(val).lower()
705
return val == 'true' or val == 'false' or \
706
val == 'yes' or val == 'no' or \
707
val == 'y' or val == 'n' or \
708
val == '1' or val == '0'
711
def is_valid_ipv4(address):
712
"""valid the address strictly as per format xxx.xxx.xxx.xxx.
713
where xxx is a value between 0 and 255.
715
parts = address.split(".")
720
if not 0 <= int(item) <= 255:
728
""" If the Flags.monkey_patch set as True,
729
this function patches a decorator
730
for all functions in specified modules.
731
You can set decorators for each modules
732
using FLAGS.monkey_patch_modules.
733
The format is "Module path:Decorator function".
734
Example: 'cinder.api.ec2.cloud:' \
735
cinder.openstack.common.notifier.api.notify_decorator'
737
Parameters of the decorator is as follows.
738
(See cinder.openstack.common.notifier.api.notify_decorator)
740
name - name of the function
741
function - object of the function
743
# If FLAGS.monkey_patch is not True, this function do nothing.
744
if not FLAGS.monkey_patch:
746
# Get list of modules and decorators
747
for module_and_decorator in FLAGS.monkey_patch_modules:
748
module, decorator_name = module_and_decorator.split(':')
749
# import decorator function
750
decorator = importutils.import_class(decorator_name)
752
# Retrieve module information using pyclbr
753
module_data = pyclbr.readmodule_ex(module)
754
for key in module_data.keys():
755
# set the decorator for the class methods
756
if isinstance(module_data[key], pyclbr.Class):
757
clz = importutils.import_class("%s.%s" % (module, key))
758
for method, func in inspect.getmembers(clz, inspect.ismethod):
760
decorator("%s.%s.%s" % (module, key, method), func))
761
# set the decorator for the function
762
if isinstance(module_data[key], pyclbr.Function):
763
func = importutils.import_class("%s.%s" % (module, key))
764
setattr(sys.modules[module], key,
765
decorator("%s.%s" % (module, key), func))
768
def convert_to_list_dict(lst, label):
769
"""Convert a value or list into a list of dicts"""
772
if not isinstance(lst, list):
774
return [{label: x} for x in lst]
778
"""Decorator that logs how long a particular function took to execute"""
779
@functools.wraps(func)
780
def inner(*args, **kwargs):
781
start_time = time.time()
783
return func(*args, **kwargs)
785
total_time = time.time() - start_time
786
LOG.debug(_("timefunc: '%(name)s' took %(total_time).2f secs") %
787
dict(name=func.__name__, total_time=total_time))
791
def generate_glance_url():
792
"""Generate the URL to glance."""
793
# TODO(jk0): This will eventually need to take SSL into consideration
794
# when supported in glance.
795
return "http://%s:%d" % (FLAGS.glance_host, FLAGS.glance_port)
798
@contextlib.contextmanager
799
def logging_error(message):
800
"""Catches exception, write message to the log, re-raise.
801
This is a common refinement of save_and_reraise that writes a specific
806
except Exception as error:
807
with excutils.save_and_reraise_exception():
808
LOG.exception(message)
811
@contextlib.contextmanager
812
def remove_path_on_error(path):
813
"""Protect code that wants to operate on PATH atomically.
814
Any exception will cause PATH to be removed.
819
with excutils.save_and_reraise_exception():
820
delete_if_exists(path)
823
def make_dev_path(dev, partition=None, base='/dev'):
824
"""Return a path to a particular device.
826
>>> make_dev_path('xvdc')
829
>>> make_dev_path('xvdc', 1)
832
path = os.path.join(base, dev)
834
path += str(partition)
838
def total_seconds(td):
839
"""Local total_seconds implementation for compatibility with python 2.6"""
840
if hasattr(td, 'total_seconds'):
841
return td.total_seconds()
843
return ((td.days * 86400 + td.seconds) * 10 ** 6 +
844
td.microseconds) / 10.0 ** 6
847
def sanitize_hostname(hostname):
848
"""Return a hostname which conforms to RFC-952 and RFC-1123 specs."""
849
if isinstance(hostname, unicode):
850
hostname = hostname.encode('latin-1', 'ignore')
852
hostname = re.sub('[ _]', '-', hostname)
853
hostname = re.sub('[^\w.-]+', '', hostname)
854
hostname = hostname.lower()
855
hostname = hostname.strip('.-')
860
def read_cached_file(filename, cache_info, reload_func=None):
861
"""Read from a file if it has been modified.
863
:param cache_info: dictionary to hold opaque cache.
864
:param reload_func: optional function to be called with data when
865
file is reloaded due to a modification.
867
:returns: data from file
870
mtime = os.path.getmtime(filename)
871
if not cache_info or mtime != cache_info.get('mtime'):
872
with open(filename) as fap:
873
cache_info['data'] = fap.read()
874
cache_info['mtime'] = mtime
876
reload_func(cache_info['data'])
877
return cache_info['data']
880
def file_open(*args, **kwargs):
883
see built-in file() documentation for more details
885
Note: The reason this is kept in a separate module is to easily
886
be able to provide a stub module that doesn't alter system
887
state at all (for unit tests)
889
return file(*args, **kwargs)
892
def hash_file(file_like_object):
893
"""Generate a hash for the contents of a file."""
894
checksum = hashlib.sha1()
895
any(map(checksum.update, iter(lambda: file_like_object.read(32768), '')))
896
return checksum.hexdigest()
899
@contextlib.contextmanager
900
def temporary_mutation(obj, **kwargs):
901
"""Temporarily set the attr on a particular object to a given value then
902
revert when finished.
904
One use of this is to temporarily set the read_deleted flag on a context
907
with temporary_mutation(context, read_deleted="yes"):
908
do_something_that_needed_deleted_objects()
910
NOT_PRESENT = object()
913
for attr, new_value in kwargs.items():
914
old_values[attr] = getattr(obj, attr, NOT_PRESENT)
915
setattr(obj, attr, new_value)
920
for attr, old_value in old_values.items():
921
if old_value is NOT_PRESENT:
924
setattr(obj, attr, old_value)
927
def service_is_up(service):
928
"""Check whether a service is up based on last heartbeat."""
929
last_heartbeat = service['updated_at'] or service['created_at']
930
# Timestamps in DB are UTC.
931
elapsed = total_seconds(timeutils.utcnow() - last_heartbeat)
932
return abs(elapsed) <= FLAGS.service_down_time
935
def generate_mac_address():
936
"""Generate an Ethernet MAC address."""
937
# NOTE(vish): We would prefer to use 0xfe here to ensure that linux
938
# bridge mac addresses don't change, but it appears to
939
# conflict with libvirt, so we use the next highest octet
940
# that has the unicast and locally administered bits set
942
# Discussion: https://bugs.launchpad.net/cinder/+bug/921838
943
mac = [0xfa, 0x16, 0x3e,
944
random.randint(0x00, 0x7f),
945
random.randint(0x00, 0xff),
946
random.randint(0x00, 0xff)]
947
return ':'.join(map(lambda x: "%02x" % x, mac))
950
def read_file_as_root(file_path):
951
"""Secure helper to read file as root."""
953
out, _err = execute('cat', file_path, run_as_root=True)
955
except exception.ProcessExecutionError:
956
raise exception.FileNotFound(file_path=file_path)
959
@contextlib.contextmanager
960
def temporary_chown(path, owner_uid=None):
961
"""Temporarily chown a path.
963
:params owner_uid: UID of temporary owner (defaults to current user)
965
if owner_uid is None:
966
owner_uid = os.getuid()
968
orig_uid = os.stat(path).st_uid
970
if orig_uid != owner_uid:
971
execute('chown', owner_uid, path, run_as_root=True)
975
if orig_uid != owner_uid:
976
execute('chown', orig_uid, path, run_as_root=True)
979
@contextlib.contextmanager
980
def tempdir(**kwargs):
981
tmpdir = tempfile.mkdtemp(**kwargs)
986
shutil.rmtree(tmpdir)
988
LOG.debug(_('Could not remove tmpdir: %s'), str(e))
991
def strcmp_const_time(s1, s2):
992
"""Constant-time string comparison.
994
:params s1: the first string
995
:params s2: the second string
997
:return: True if the strings are equal.
999
This function takes two strings and compares them. It is intended to be
1000
used when doing a comparison for authentication purposes to help guard
1001
against timing attacks.
1003
if len(s1) != len(s2):
1006
for (a, b) in zip(s1, s2):
1007
result |= ord(a) ^ ord(b)
1011
def walk_class_hierarchy(clazz, encountered=None):
1012
"""Walk class hierarchy, yielding most derived classes first"""
1015
for subclass in clazz.__subclasses__():
1016
if subclass not in encountered:
1017
encountered.append(subclass)
1018
# drill down to leaves first
1019
for subsubclass in walk_class_hierarchy(subclass, encountered):
1024
class UndoManager(object):
1025
"""Provides a mechanism to facilitate rolling back a series of actions
1026
when an exception is raised.
1029
self.undo_stack = []
1031
def undo_with(self, undo_func):
1032
self.undo_stack.append(undo_func)
1034
def _rollback(self):
1035
for undo_func in reversed(self.undo_stack):
1038
def rollback_and_reraise(self, msg=None, **kwargs):
1039
"""Rollback a series of actions then re-raise the exception.
1041
.. note:: (sirp) This should only be called within an
1044
with excutils.save_and_reraise_exception():
1046
LOG.exception(msg, **kwargs)
1051
def ensure_tree(path):
1052
"""Create a directory (and any ancestor directories required)
1054
:param path: Directory to create
1058
except OSError as exc:
1059
if exc.errno == errno.EEXIST:
1060
if not os.path.isdir(path):