97
by James Page
* Re-sync with latest security updates. |
1 |
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
2 |
||
3 |
# Copyright 2010 United States Government as represented by the
|
|
4 |
# Administrator of the National Aeronautics and Space Administration.
|
|
5 |
# Copyright 2011 Justin Santa Barbara
|
|
6 |
# All Rights Reserved.
|
|
7 |
#
|
|
8 |
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
9 |
# not use this file except in compliance with the License. You may obtain
|
|
10 |
# a copy of the License at
|
|
11 |
#
|
|
12 |
# http://www.apache.org/licenses/LICENSE-2.0
|
|
13 |
#
|
|
14 |
# Unless required by applicable law or agreed to in writing, software
|
|
15 |
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
16 |
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
17 |
# License for the specific language governing permissions and limitations
|
|
18 |
# under the License.
|
|
19 |
||
20 |
"""Utilities and helper functions."""
|
|
21 |
||
22 |
import contextlib |
|
23 |
import datetime |
|
24 |
import errno |
|
25 |
import functools |
|
26 |
import hashlib |
|
27 |
import inspect |
|
28 |
import os |
|
29 |
import pyclbr |
|
30 |
import random |
|
31 |
import re |
|
32 |
import shlex |
|
33 |
import shutil |
|
34 |
import signal |
|
35 |
import socket |
|
36 |
import struct |
|
37 |
import sys |
|
38 |
import tempfile |
|
39 |
import time |
|
40 |
import uuid |
|
41 |
import weakref |
|
42 |
from xml.sax import saxutils |
|
43 |
||
44 |
from eventlet import event |
|
45 |
from eventlet.green import subprocess |
|
46 |
from eventlet import greenthread |
|
47 |
from eventlet import semaphore |
|
48 |
import netaddr |
|
49 |
||
50 |
from nova.common import deprecated |
|
51 |
from nova import exception |
|
52 |
from nova import flags |
|
53 |
from nova.openstack.common import cfg |
|
54 |
from nova.openstack.common import excutils |
|
55 |
from nova.openstack.common import importutils |
|
56 |
from nova.openstack.common import log as logging |
|
57 |
from nova.openstack.common import timeutils |
|
58 |
||
59 |
||
60 |
LOG = logging.getLogger(__name__) |
|
61 |
FLAGS = flags.FLAGS |
|
62 |
||
63 |
FLAGS.register_opt( |
|
64 |
cfg.BoolOpt('disable_process_locking', default=False, |
|
65 |
help='Whether to disable inter-process locks')) |
|
66 |
||
67 |
||
68 |
def vpn_ping(address, port, timeout=0.05, session_id=None): |
|
69 |
"""Sends a vpn negotiation packet and returns the server session.
|
|
70 |
||
71 |
Returns False on a failure. Basic packet structure is below.
|
|
72 |
||
73 |
Client packet (14 bytes)::
|
|
74 |
||
75 |
0 1 8 9 13
|
|
76 |
+-+--------+-----+
|
|
77 |
|x| cli_id |?????|
|
|
78 |
+-+--------+-----+
|
|
79 |
x = packet identifier 0x38
|
|
80 |
cli_id = 64 bit identifier
|
|
81 |
? = unknown, probably flags/padding
|
|
82 |
||
83 |
Server packet (26 bytes)::
|
|
84 |
||
85 |
0 1 8 9 13 14 21 2225
|
|
86 |
+-+--------+-----+--------+----+
|
|
87 |
|x| srv_id |?????| cli_id |????|
|
|
88 |
+-+--------+-----+--------+----+
|
|
89 |
x = packet identifier 0x40
|
|
90 |
cli_id = 64 bit identifier
|
|
91 |
? = unknown, probably flags/padding
|
|
92 |
bit 9 was 1 and the rest were 0 in testing
|
|
93 |
||
94 |
"""
|
|
95 |
if session_id is None: |
|
96 |
session_id = random.randint(0, 0xffffffffffffffff) |
|
97 |
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) |
|
98 |
data = struct.pack('!BQxxxxx', 0x38, session_id) |
|
99 |
sock.sendto(data, (address, port)) |
|
100 |
sock.settimeout(timeout) |
|
101 |
try: |
|
102 |
received = sock.recv(2048) |
|
103 |
except socket.timeout: |
|
104 |
return False |
|
105 |
finally: |
|
106 |
sock.close() |
|
107 |
fmt = '!BQxxxxxQxxxx' |
|
108 |
if len(received) != struct.calcsize(fmt): |
|
109 |
print struct.calcsize(fmt) |
|
110 |
return False |
|
111 |
(identifier, server_sess, client_sess) = struct.unpack(fmt, received) |
|
112 |
if identifier == 0x40 and client_sess == session_id: |
|
113 |
return server_sess |
|
114 |
||
115 |
||
116 |
def _subprocess_setup(): |
|
117 |
# Python installs a SIGPIPE handler by default. This is usually not what
|
|
118 |
# non-Python subprocesses expect.
|
|
119 |
signal.signal(signal.SIGPIPE, signal.SIG_DFL) |
|
120 |
||
121 |
||
122 |
def execute(*cmd, **kwargs): |
|
123 |
"""Helper method to execute command with optional retry.
|
|
124 |
||
125 |
If you add a run_as_root=True command, don't forget to add the
|
|
126 |
corresponding filter to etc/nova/rootwrap.d !
|
|
127 |
||
128 |
:param cmd: Passed to subprocess.Popen.
|
|
129 |
:param process_input: Send to opened process.
|
|
130 |
:param check_exit_code: Single bool, int, or list of allowed exit
|
|
131 |
codes. Defaults to [0]. Raise
|
|
132 |
exception.ProcessExecutionError unless
|
|
133 |
program exits with one of these code.
|
|
134 |
:param delay_on_retry: True | False. Defaults to True. If set to
|
|
135 |
True, wait a short amount of time
|
|
136 |
before retrying.
|
|
137 |
:param attempts: How many times to retry cmd.
|
|
138 |
:param run_as_root: True | False. Defaults to False. If set to True,
|
|
139 |
the command is prefixed by the command specified
|
|
140 |
in the root_helper FLAG.
|
|
141 |
||
142 |
:raises exception.NovaException: on receiving unknown arguments
|
|
143 |
:raises exception.ProcessExecutionError:
|
|
144 |
||
145 |
:returns: a tuple, (stdout, stderr) from the spawned process, or None if
|
|
146 |
the command fails.
|
|
147 |
"""
|
|
148 |
process_input = kwargs.pop('process_input', None) |
|
149 |
check_exit_code = kwargs.pop('check_exit_code', [0]) |
|
150 |
ignore_exit_code = False |
|
151 |
if isinstance(check_exit_code, bool): |
|
152 |
ignore_exit_code = not check_exit_code |
|
153 |
check_exit_code = [0] |
|
154 |
elif isinstance(check_exit_code, int): |
|
155 |
check_exit_code = [check_exit_code] |
|
156 |
delay_on_retry = kwargs.pop('delay_on_retry', True) |
|
157 |
attempts = kwargs.pop('attempts', 1) |
|
158 |
run_as_root = kwargs.pop('run_as_root', False) |
|
159 |
shell = kwargs.pop('shell', False) |
|
160 |
||
161 |
if len(kwargs): |
|
162 |
raise exception.NovaException(_('Got unknown keyword args ' |
|
163 |
'to utils.execute: %r') % kwargs) |
|
164 |
||
165 |
if run_as_root: |
|
166 |
||
167 |
if FLAGS.rootwrap_config is None or FLAGS.root_helper != 'sudo': |
|
168 |
deprecated.warn(_('The root_helper option (which lets you specify ' |
|
169 |
'a root wrapper different from nova-rootwrap, '
|
|
170 |
'and defaults to using sudo) is now deprecated. '
|
|
171 |
'You should use the rootwrap_config option '
|
|
172 |
'instead.')) |
|
173 |
||
174 |
if (FLAGS.rootwrap_config is not None): |
|
175 |
cmd = ['sudo', 'nova-rootwrap', FLAGS.rootwrap_config] + list(cmd) |
|
176 |
else: |
|
177 |
cmd = shlex.split(FLAGS.root_helper) + list(cmd) |
|
178 |
cmd = map(str, cmd) |
|
179 |
||
180 |
while attempts > 0: |
|
181 |
attempts -= 1 |
|
182 |
try: |
|
183 |
LOG.debug(_('Running cmd (subprocess): %s'), ' '.join(cmd)) |
|
184 |
_PIPE = subprocess.PIPE # pylint: disable=E1101 |
|
185 |
obj = subprocess.Popen(cmd, |
|
186 |
stdin=_PIPE, |
|
187 |
stdout=_PIPE, |
|
188 |
stderr=_PIPE, |
|
189 |
close_fds=True, |
|
190 |
preexec_fn=_subprocess_setup, |
|
191 |
shell=shell) |
|
192 |
result = None |
|
193 |
if process_input is not None: |
|
194 |
result = obj.communicate(process_input) |
|
195 |
else: |
|
196 |
result = obj.communicate() |
|
197 |
obj.stdin.close() # pylint: disable=E1101 |
|
198 |
_returncode = obj.returncode # pylint: disable=E1101 |
|
199 |
LOG.debug(_('Result was %s') % _returncode) |
|
200 |
if not ignore_exit_code and _returncode not in check_exit_code: |
|
201 |
(stdout, stderr) = result |
|
202 |
raise exception.ProcessExecutionError( |
|
203 |
exit_code=_returncode, |
|
204 |
stdout=stdout, |
|
205 |
stderr=stderr, |
|
206 |
cmd=' '.join(cmd)) |
|
207 |
return result |
|
208 |
except exception.ProcessExecutionError: |
|
209 |
if not attempts: |
|
210 |
raise
|
|
211 |
else: |
|
212 |
LOG.debug(_('%r failed. Retrying.'), cmd) |
|
213 |
if delay_on_retry: |
|
214 |
greenthread.sleep(random.randint(20, 200) / 100.0) |
|
215 |
finally: |
|
216 |
# NOTE(termie): this appears to be necessary to let the subprocess
|
|
217 |
# call clean something up in between calls, without
|
|
218 |
# it two execute calls in a row hangs the second one
|
|
219 |
greenthread.sleep(0) |
|
220 |
||
221 |
||
222 |
def trycmd(*args, **kwargs): |
|
223 |
"""
|
|
224 |
A wrapper around execute() to more easily handle warnings and errors.
|
|
225 |
||
226 |
Returns an (out, err) tuple of strings containing the output of
|
|
227 |
the command's stdout and stderr. If 'err' is not empty then the
|
|
228 |
command can be considered to have failed.
|
|
229 |
||
230 |
:discard_warnings True | False. Defaults to False. If set to True,
|
|
231 |
then for succeeding commands, stderr is cleared
|
|
232 |
||
233 |
"""
|
|
234 |
discard_warnings = kwargs.pop('discard_warnings', False) |
|
235 |
||
236 |
try: |
|
237 |
out, err = execute(*args, **kwargs) |
|
238 |
failed = False |
|
239 |
except exception.ProcessExecutionError, exn: |
|
240 |
out, err = '', str(exn) |
|
241 |
failed = True |
|
242 |
||
243 |
if not failed and discard_warnings and err: |
|
244 |
# Handle commands that output to stderr but otherwise succeed
|
|
245 |
err = '' |
|
246 |
||
247 |
return out, err |
|
248 |
||
249 |
||
250 |
def ssh_execute(ssh, cmd, process_input=None, |
|
251 |
addl_env=None, check_exit_code=True): |
|
252 |
LOG.debug(_('Running cmd (SSH): %s'), ' '.join(cmd)) |
|
253 |
if addl_env: |
|
254 |
raise exception.NovaException(_('Environment not supported over SSH')) |
|
255 |
||
256 |
if process_input: |
|
257 |
# This is (probably) fixable if we need it...
|
|
258 |
msg = _('process_input not supported over SSH') |
|
259 |
raise exception.NovaException(msg) |
|
260 |
||
261 |
stdin_stream, stdout_stream, stderr_stream = ssh.exec_command(cmd) |
|
262 |
channel = stdout_stream.channel |
|
263 |
||
264 |
#stdin.write('process_input would go here')
|
|
265 |
#stdin.flush()
|
|
266 |
||
267 |
# NOTE(justinsb): This seems suspicious...
|
|
268 |
# ...other SSH clients have buffering issues with this approach
|
|
269 |
stdout = stdout_stream.read() |
|
270 |
stderr = stderr_stream.read() |
|
271 |
stdin_stream.close() |
|
272 |
||
273 |
exit_status = channel.recv_exit_status() |
|
274 |
||
275 |
# exit_status == -1 if no exit code was returned
|
|
276 |
if exit_status != -1: |
|
277 |
LOG.debug(_('Result was %s') % exit_status) |
|
278 |
if check_exit_code and exit_status != 0: |
|
279 |
raise exception.ProcessExecutionError(exit_code=exit_status, |
|
280 |
stdout=stdout, |
|
281 |
stderr=stderr, |
|
282 |
cmd=' '.join(cmd)) |
|
283 |
||
284 |
return (stdout, stderr) |
|
285 |
||
286 |
||
287 |
def novadir(): |
|
288 |
import nova |
|
289 |
return os.path.abspath(nova.__file__).split('nova/__init__.py')[0] |
|
290 |
||
291 |
||
292 |
def debug(arg): |
|
293 |
LOG.debug(_('debug in callback: %s'), arg) |
|
294 |
return arg |
|
295 |
||
296 |
||
297 |
def generate_uid(topic, size=8): |
|
298 |
characters = '01234567890abcdefghijklmnopqrstuvwxyz' |
|
299 |
choices = [random.choice(characters) for _x in xrange(size)] |
|
300 |
return '%s-%s' % (topic, ''.join(choices)) |
|
301 |
||
302 |
||
303 |
# Default symbols to use for passwords. Avoids visually confusing characters.
|
|
304 |
# ~6 bits per symbol
|
|
305 |
DEFAULT_PASSWORD_SYMBOLS = ('23456789', # Removed: 0,1 |
|
306 |
'ABCDEFGHJKLMNPQRSTUVWXYZ', # Removed: I, O |
|
307 |
'abcdefghijkmnopqrstuvwxyz') # Removed: l |
|
308 |
||
309 |
||
310 |
# ~5 bits per symbol
|
|
311 |
EASIER_PASSWORD_SYMBOLS = ('23456789', # Removed: 0, 1 |
|
312 |
'ABCDEFGHJKLMNPQRSTUVWXYZ') # Removed: I, O |
|
313 |
||
314 |
||
315 |
def last_completed_audit_period(unit=None, before=None): |
|
316 |
"""This method gives you the most recently *completed* audit period.
|
|
317 |
||
318 |
arguments:
|
|
319 |
units: string, one of 'hour', 'day', 'month', 'year'
|
|
320 |
Periods normally begin at the beginning (UTC) of the
|
|
321 |
period unit (So a 'day' period begins at midnight UTC,
|
|
322 |
a 'month' unit on the 1st, a 'year' on Jan, 1)
|
|
323 |
unit string may be appended with an optional offset
|
|
324 |
like so: 'day@18' This will begin the period at 18:00
|
|
325 |
UTC. 'month@15' starts a monthly period on the 15th,
|
|
326 |
and year@3 begins a yearly one on March 1st.
|
|
327 |
before: Give the audit period most recently completed before
|
|
328 |
<timestamp>. Defaults to now.
|
|
329 |
||
330 |
||
331 |
returns: 2 tuple of datetimes (begin, end)
|
|
332 |
The begin timestamp of this audit period is the same as the
|
|
333 |
end of the previous."""
|
|
334 |
if not unit: |
|
335 |
unit = FLAGS.instance_usage_audit_period |
|
336 |
||
337 |
offset = 0 |
|
338 |
if '@' in unit: |
|
339 |
unit, offset = unit.split("@", 1) |
|
340 |
offset = int(offset) |
|
341 |
||
342 |
if before is not None: |
|
343 |
rightnow = before |
|
344 |
else: |
|
345 |
rightnow = timeutils.utcnow() |
|
346 |
if unit not in ('month', 'day', 'year', 'hour'): |
|
347 |
raise ValueError('Time period must be hour, day, month or year') |
|
348 |
if unit == 'month': |
|
349 |
if offset == 0: |
|
350 |
offset = 1 |
|
351 |
end = datetime.datetime(day=offset, |
|
352 |
month=rightnow.month, |
|
353 |
year=rightnow.year) |
|
354 |
if end >= rightnow: |
|
355 |
year = rightnow.year |
|
356 |
if 1 >= rightnow.month: |
|
357 |
year -= 1 |
|
358 |
month = 12 + (rightnow.month - 1) |
|
359 |
else: |
|
360 |
month = rightnow.month - 1 |
|
361 |
end = datetime.datetime(day=offset, |
|
362 |
month=month, |
|
363 |
year=year) |
|
364 |
year = end.year |
|
365 |
if 1 >= end.month: |
|
366 |
year -= 1 |
|
367 |
month = 12 + (end.month - 1) |
|
368 |
else: |
|
369 |
month = end.month - 1 |
|
370 |
begin = datetime.datetime(day=offset, month=month, year=year) |
|
371 |
||
372 |
elif unit == 'year': |
|
373 |
if offset == 0: |
|
374 |
offset = 1 |
|
375 |
end = datetime.datetime(day=1, month=offset, year=rightnow.year) |
|
376 |
if end >= rightnow: |
|
377 |
end = datetime.datetime(day=1, |
|
378 |
month=offset, |
|
379 |
year=rightnow.year - 1) |
|
380 |
begin = datetime.datetime(day=1, |
|
381 |
month=offset, |
|
382 |
year=rightnow.year - 2) |
|
383 |
else: |
|
384 |
begin = datetime.datetime(day=1, |
|
385 |
month=offset, |
|
386 |
year=rightnow.year - 1) |
|
387 |
||
388 |
elif unit == 'day': |
|
389 |
end = datetime.datetime(hour=offset, |
|
390 |
day=rightnow.day, |
|
391 |
month=rightnow.month, |
|
392 |
year=rightnow.year) |
|
393 |
if end >= rightnow: |
|
394 |
end = end - datetime.timedelta(days=1) |
|
395 |
begin = end - datetime.timedelta(days=1) |
|
396 |
||
397 |
elif unit == 'hour': |
|
398 |
end = rightnow.replace(minute=offset, second=0, microsecond=0) |
|
399 |
if end >= rightnow: |
|
400 |
end = end - datetime.timedelta(hours=1) |
|
401 |
begin = end - datetime.timedelta(hours=1) |
|
402 |
||
403 |
return (begin, end) |
|
404 |
||
405 |
||
406 |
def generate_password(length=20, symbolgroups=DEFAULT_PASSWORD_SYMBOLS): |
|
407 |
"""Generate a random password from the supplied symbol groups.
|
|
408 |
||
409 |
At least one symbol from each group will be included. Unpredictable
|
|
410 |
results if length is less than the number of symbol groups.
|
|
411 |
||
412 |
Believed to be reasonably secure (with a reasonable password length!)
|
|
413 |
||
414 |
"""
|
|
415 |
r = random.SystemRandom() |
|
416 |
||
417 |
# NOTE(jerdfelt): Some password policies require at least one character
|
|
418 |
# from each group of symbols, so start off with one random character
|
|
419 |
# from each symbol group
|
|
420 |
password = [r.choice(s) for s in symbolgroups] |
|
421 |
# If length < len(symbolgroups), the leading characters will only
|
|
422 |
# be from the first length groups. Try our best to not be predictable
|
|
423 |
# by shuffling and then truncating.
|
|
424 |
r.shuffle(password) |
|
425 |
password = password[:length] |
|
426 |
length -= len(password) |
|
427 |
||
428 |
# then fill with random characters from all symbol groups
|
|
429 |
symbols = ''.join(symbolgroups) |
|
430 |
password.extend([r.choice(symbols) for _i in xrange(length)]) |
|
431 |
||
432 |
# finally shuffle to ensure first x characters aren't from a
|
|
433 |
# predictable group
|
|
434 |
r.shuffle(password) |
|
435 |
||
436 |
return ''.join(password) |
|
437 |
||
438 |
||
439 |
def last_octet(address): |
|
440 |
return int(address.split('.')[-1]) |
|
441 |
||
442 |
||
443 |
def get_my_linklocal(interface): |
|
444 |
try: |
|
445 |
if_str = execute('ip', '-f', 'inet6', '-o', 'addr', 'show', interface) |
|
446 |
condition = '\s+inet6\s+([0-9a-f:]+)/\d+\s+scope\s+link' |
|
447 |
links = [re.search(condition, x) for x in if_str[0].split('\n')] |
|
448 |
address = [w.group(1) for w in links if w is not None] |
|
449 |
if address[0] is not None: |
|
450 |
return address[0] |
|
451 |
else: |
|
452 |
msg = _('Link Local address is not found.:%s') % if_str |
|
453 |
raise exception.NovaException(msg) |
|
454 |
except Exception as ex: |
|
455 |
msg = _("Couldn't get Link Local IP of %(interface)s" |
|
456 |
" :%(ex)s") % locals() |
|
457 |
raise exception.NovaException(msg) |
|
458 |
||
459 |
||
460 |
def parse_mailmap(mailmap='.mailmap'): |
|
461 |
mapping = {} |
|
462 |
if os.path.exists(mailmap): |
|
463 |
fp = open(mailmap, 'r') |
|
464 |
for l in fp: |
|
465 |
l = l.strip() |
|
466 |
if not l.startswith('#') and ' ' in l: |
|
467 |
canonical_email, alias = l.split(' ') |
|
468 |
mapping[alias.lower()] = canonical_email.lower() |
|
469 |
return mapping |
|
470 |
||
471 |
||
472 |
def str_dict_replace(s, mapping): |
|
473 |
for s1, s2 in mapping.iteritems(): |
|
474 |
s = s.replace(s1, s2) |
|
475 |
return s |
|
476 |
||
477 |
||
478 |
class LazyPluggable(object): |
|
479 |
"""A pluggable backend loaded lazily based on some value."""
|
|
480 |
||
481 |
def __init__(self, pivot, **backends): |
|
482 |
self.__backends = backends |
|
483 |
self.__pivot = pivot |
|
484 |
self.__backend = None |
|
485 |
||
486 |
def __get_backend(self): |
|
487 |
if not self.__backend: |
|
488 |
backend_name = FLAGS[self.__pivot] |
|
489 |
if backend_name not in self.__backends: |
|
490 |
msg = _('Invalid backend: %s') % backend_name |
|
491 |
raise exception.NovaException(msg) |
|
492 |
||
493 |
backend = self.__backends[backend_name] |
|
494 |
if isinstance(backend, tuple): |
|
495 |
name = backend[0] |
|
496 |
fromlist = backend[1] |
|
497 |
else: |
|
498 |
name = backend |
|
499 |
fromlist = backend |
|
500 |
||
501 |
self.__backend = __import__(name, None, None, fromlist) |
|
502 |
LOG.debug(_('backend %s'), self.__backend) |
|
503 |
return self.__backend |
|
504 |
||
505 |
def __getattr__(self, key): |
|
506 |
backend = self.__get_backend() |
|
507 |
return getattr(backend, key) |
|
508 |
||
509 |
||
510 |
class LoopingCallDone(Exception): |
|
511 |
"""Exception to break out and stop a LoopingCall.
|
|
512 |
||
513 |
The poll-function passed to LoopingCall can raise this exception to
|
|
514 |
break out of the loop normally. This is somewhat analogous to
|
|
515 |
StopIteration.
|
|
516 |
||
517 |
An optional return-value can be included as the argument to the exception;
|
|
518 |
this return-value will be returned by LoopingCall.wait()
|
|
519 |
||
520 |
"""
|
|
521 |
||
522 |
def __init__(self, retvalue=True): |
|
523 |
""":param retvalue: Value that LoopingCall.wait() should return."""
|
|
524 |
self.retvalue = retvalue |
|
525 |
||
526 |
||
527 |
class LoopingCall(object): |
|
528 |
def __init__(self, f=None, *args, **kw): |
|
529 |
self.args = args |
|
530 |
self.kw = kw |
|
531 |
self.f = f |
|
532 |
self._running = False |
|
533 |
||
534 |
def start(self, interval, initial_delay=None): |
|
535 |
self._running = True |
|
536 |
done = event.Event() |
|
537 |
||
538 |
def _inner(): |
|
539 |
if initial_delay: |
|
540 |
greenthread.sleep(initial_delay) |
|
541 |
||
542 |
try: |
|
543 |
while self._running: |
|
544 |
self.f(*self.args, **self.kw) |
|
545 |
if not self._running: |
|
546 |
break
|
|
547 |
greenthread.sleep(interval) |
|
548 |
except LoopingCallDone, e: |
|
549 |
self.stop() |
|
550 |
done.send(e.retvalue) |
|
551 |
except Exception: |
|
552 |
LOG.exception(_('in looping call')) |
|
553 |
done.send_exception(*sys.exc_info()) |
|
554 |
return
|
|
555 |
else: |
|
556 |
done.send(True) |
|
557 |
||
558 |
self.done = done |
|
559 |
||
560 |
greenthread.spawn(_inner) |
|
561 |
return self.done |
|
562 |
||
563 |
def stop(self): |
|
564 |
self._running = False |
|
565 |
||
566 |
def wait(self): |
|
567 |
return self.done.wait() |
|
568 |
||
569 |
||
570 |
def xhtml_escape(value): |
|
571 |
"""Escapes a string so it is valid within XML or XHTML.
|
|
572 |
||
573 |
"""
|
|
574 |
return saxutils.escape(value, {'"': '"', "'": '''}) |
|
575 |
||
576 |
||
577 |
def utf8(value): |
|
578 |
"""Try to turn a string into utf-8 if possible.
|
|
579 |
||
580 |
Code is directly from the utf8 function in
|
|
581 |
http://github.com/facebook/tornado/blob/master/tornado/escape.py
|
|
582 |
||
583 |
"""
|
|
584 |
if isinstance(value, unicode): |
|
585 |
return value.encode('utf-8') |
|
586 |
assert isinstance(value, str) |
|
587 |
return value |
|
588 |
||
589 |
||
590 |
class _InterProcessLock(object): |
|
591 |
"""Lock implementation which allows multiple locks, working around
|
|
592 |
issues like bugs.debian.org/cgi-bin/bugreport.cgi?bug=632857 and does
|
|
593 |
not require any cleanup. Since the lock is always held on a file
|
|
594 |
descriptor rather than outside of the process, the lock gets dropped
|
|
595 |
automatically if the process crashes, even if __exit__ is not executed.
|
|
596 |
||
597 |
There are no guarantees regarding usage by multiple green threads in a
|
|
598 |
single process here. This lock works only between processes. Exclusive
|
|
599 |
access between local threads should be achieved using the semaphores
|
|
600 |
in the @synchronized decorator.
|
|
601 |
||
602 |
Note these locks are released when the descriptor is closed, so it's not
|
|
603 |
safe to close the file descriptor while another green thread holds the
|
|
604 |
lock. Just opening and closing the lock file can break synchronisation,
|
|
605 |
so lock files must be accessed only using this abstraction.
|
|
606 |
"""
|
|
607 |
||
608 |
def __init__(self, name): |
|
609 |
self.lockfile = None |
|
610 |
self.fname = name |
|
611 |
||
612 |
def __enter__(self): |
|
613 |
self.lockfile = open(self.fname, 'w') |
|
614 |
||
615 |
while True: |
|
616 |
try: |
|
617 |
# Using non-blocking locks since green threads are not
|
|
618 |
# patched to deal with blocking locking calls.
|
|
619 |
# Also upon reading the MSDN docs for locking(), it seems
|
|
620 |
# to have a laughable 10 attempts "blocking" mechanism.
|
|
621 |
self.trylock() |
|
622 |
return self |
|
623 |
except IOError, e: |
|
624 |
if e.errno in (errno.EACCES, errno.EAGAIN): |
|
625 |
# external locks synchronise things like iptables
|
|
626 |
# updates - give it some time to prevent busy spinning
|
|
627 |
time.sleep(0.01) |
|
628 |
else: |
|
629 |
raise
|
|
630 |
||
631 |
def __exit__(self, exc_type, exc_val, exc_tb): |
|
632 |
try: |
|
633 |
self.unlock() |
|
634 |
self.lockfile.close() |
|
635 |
except IOError: |
|
636 |
LOG.exception(_("Could not release the acquired lock `%s`") |
|
637 |
% self.fname) |
|
638 |
||
639 |
def trylock(self): |
|
640 |
raise NotImplementedError() |
|
641 |
||
642 |
def unlock(self): |
|
643 |
raise NotImplementedError() |
|
644 |
||
645 |
||
646 |
class _WindowsLock(_InterProcessLock): |
|
647 |
def trylock(self): |
|
648 |
msvcrt.locking(self.lockfile, msvcrt.LK_NBLCK, 1) |
|
649 |
||
650 |
def unlock(self): |
|
651 |
msvcrt.locking(self.lockfile, msvcrt.LK_UNLCK, 1) |
|
652 |
||
653 |
||
654 |
class _PosixLock(_InterProcessLock): |
|
655 |
def trylock(self): |
|
656 |
fcntl.lockf(self.lockfile, fcntl.LOCK_EX | fcntl.LOCK_NB) |
|
657 |
||
658 |
def unlock(self): |
|
659 |
fcntl.lockf(self.lockfile, fcntl.LOCK_UN) |
|
660 |
||
661 |
||
662 |
if os.name == 'nt': |
|
663 |
import msvcrt |
|
664 |
InterProcessLock = _WindowsLock |
|
665 |
else: |
|
666 |
import fcntl |
|
667 |
InterProcessLock = _PosixLock |
|
668 |
||
669 |
_semaphores = weakref.WeakValueDictionary() |
|
670 |
||
671 |
||
672 |
def synchronized(name, external=False, lock_path=None): |
|
673 |
"""Synchronization decorator.
|
|
674 |
||
675 |
Decorating a method like so::
|
|
676 |
||
677 |
@synchronized('mylock')
|
|
678 |
def foo(self, *args):
|
|
679 |
...
|
|
680 |
||
681 |
ensures that only one thread will execute the bar method at a time.
|
|
682 |
||
683 |
Different methods can share the same lock::
|
|
684 |
||
685 |
@synchronized('mylock')
|
|
686 |
def foo(self, *args):
|
|
687 |
...
|
|
688 |
||
689 |
@synchronized('mylock')
|
|
690 |
def bar(self, *args):
|
|
691 |
...
|
|
692 |
||
693 |
This way only one of either foo or bar can be executing at a time.
|
|
694 |
||
695 |
The external keyword argument denotes whether this lock should work across
|
|
696 |
multiple processes. This means that if two different workers both run a
|
|
697 |
a method decorated with @synchronized('mylock', external=True), only one
|
|
698 |
of them will execute at a time.
|
|
699 |
||
700 |
The lock_path keyword argument is used to specify a special location for
|
|
701 |
external lock files to live. If nothing is set, then FLAGS.lock_path is
|
|
702 |
used as a default.
|
|
703 |
"""
|
|
704 |
||
705 |
def wrap(f): |
|
706 |
@functools.wraps(f) |
|
707 |
def inner(*args, **kwargs): |
|
708 |
# NOTE(soren): If we ever go natively threaded, this will be racy.
|
|
709 |
# See http://stackoverflow.com/questions/5390569/dyn
|
|
710 |
# amically-allocating-and-destroying-mutexes
|
|
711 |
sem = _semaphores.get(name, semaphore.Semaphore()) |
|
712 |
if name not in _semaphores: |
|
713 |
# this check is not racy - we're already holding ref locally
|
|
714 |
# so GC won't remove the item and there was no IO switch
|
|
715 |
# (only valid in greenthreads)
|
|
716 |
_semaphores[name] = sem |
|
717 |
||
718 |
with sem: |
|
719 |
LOG.debug(_('Got semaphore "%(lock)s" for method ' |
|
720 |
'"%(method)s"...'), {'lock': name, |
|
721 |
'method': f.__name__}) |
|
722 |
if external and not FLAGS.disable_process_locking: |
|
723 |
LOG.debug(_('Attempting to grab file lock "%(lock)s" for ' |
|
724 |
'method "%(method)s"...'), |
|
725 |
{'lock': name, 'method': f.__name__}) |
|
726 |
cleanup_dir = False |
|
727 |
||
728 |
# We need a copy of lock_path because it is non-local
|
|
729 |
local_lock_path = lock_path |
|
730 |
if not local_lock_path: |
|
731 |
local_lock_path = FLAGS.lock_path |
|
732 |
||
733 |
if not local_lock_path: |
|
734 |
cleanup_dir = True |
|
735 |
local_lock_path = tempfile.mkdtemp() |
|
736 |
||
737 |
if not os.path.exists(local_lock_path): |
|
738 |
cleanup_dir = True |
|
739 |
ensure_tree(local_lock_path) |
|
740 |
||
741 |
# NOTE(mikal): the lock name cannot contain directory
|
|
742 |
# separators
|
|
743 |
safe_name = name.replace(os.sep, '_') |
|
744 |
lock_file_path = os.path.join(local_lock_path, |
|
745 |
'nova-%s' % safe_name) |
|
746 |
try: |
|
747 |
lock = InterProcessLock(lock_file_path) |
|
748 |
with lock: |
|
749 |
LOG.debug(_('Got file lock "%(lock)s" for ' |
|
750 |
'method "%(method)s"...'), |
|
751 |
{'lock': name, 'method': f.__name__}) |
|
752 |
retval = f(*args, **kwargs) |
|
753 |
finally: |
|
754 |
# NOTE(vish): This removes the tempdir if we needed
|
|
755 |
# to create one. This is used to cleanup
|
|
756 |
# the locks left behind by unit tests.
|
|
757 |
if cleanup_dir: |
|
758 |
shutil.rmtree(local_lock_path) |
|
759 |
else: |
|
760 |
retval = f(*args, **kwargs) |
|
761 |
||
762 |
return retval |
|
763 |
return inner |
|
764 |
return wrap |
|
765 |
||
766 |
||
767 |
def delete_if_exists(pathname): |
|
768 |
"""delete a file, but ignore file not found error"""
|
|
769 |
||
770 |
try: |
|
771 |
os.unlink(pathname) |
|
772 |
except OSError as e: |
|
773 |
if e.errno == errno.ENOENT: |
|
774 |
return
|
|
775 |
else: |
|
776 |
raise
|
|
777 |
||
778 |
||
779 |
def get_from_path(items, path): |
|
780 |
"""Returns a list of items matching the specified path.
|
|
781 |
||
782 |
Takes an XPath-like expression e.g. prop1/prop2/prop3, and for each item
|
|
783 |
in items, looks up items[prop1][prop2][prop3]. Like XPath, if any of the
|
|
784 |
intermediate results are lists it will treat each list item individually.
|
|
785 |
A 'None' in items or any child expressions will be ignored, this function
|
|
786 |
will not throw because of None (anywhere) in items. The returned list
|
|
787 |
will contain no None values.
|
|
788 |
||
789 |
"""
|
|
790 |
if path is None: |
|
791 |
raise exception.NovaException('Invalid mini_xpath') |
|
792 |
||
793 |
(first_token, sep, remainder) = path.partition('/') |
|
794 |
||
795 |
if first_token == '': |
|
796 |
raise exception.NovaException('Invalid mini_xpath') |
|
797 |
||
798 |
results = [] |
|
799 |
||
800 |
if items is None: |
|
801 |
return results |
|
802 |
||
803 |
if not isinstance(items, list): |
|
804 |
# Wrap single objects in a list
|
|
805 |
items = [items] |
|
806 |
||
807 |
for item in items: |
|
808 |
if item is None: |
|
809 |
continue
|
|
810 |
get_method = getattr(item, 'get', None) |
|
811 |
if get_method is None: |
|
812 |
continue
|
|
813 |
child = get_method(first_token) |
|
814 |
if child is None: |
|
815 |
continue
|
|
816 |
if isinstance(child, list): |
|
817 |
# Flatten intermediate lists
|
|
818 |
for x in child: |
|
819 |
results.append(x) |
|
820 |
else: |
|
821 |
results.append(child) |
|
822 |
||
823 |
if not sep: |
|
824 |
# No more tokens
|
|
825 |
return results |
|
826 |
else: |
|
827 |
return get_from_path(results, remainder) |
|
828 |
||
829 |
||
830 |
def flatten_dict(dict_, flattened=None): |
|
831 |
"""Recursively flatten a nested dictionary."""
|
|
832 |
flattened = flattened or {} |
|
833 |
for key, value in dict_.iteritems(): |
|
834 |
if hasattr(value, 'iteritems'): |
|
835 |
flatten_dict(value, flattened) |
|
836 |
else: |
|
837 |
flattened[key] = value |
|
838 |
return flattened |
|
839 |
||
840 |
||
841 |
def partition_dict(dict_, keys): |
|
842 |
"""Return two dicts, one with `keys` the other with everything else."""
|
|
843 |
intersection = {} |
|
844 |
difference = {} |
|
845 |
for key, value in dict_.iteritems(): |
|
846 |
if key in keys: |
|
847 |
intersection[key] = value |
|
848 |
else: |
|
849 |
difference[key] = value |
|
850 |
return intersection, difference |
|
851 |
||
852 |
||
853 |
def map_dict_keys(dict_, key_map): |
|
854 |
"""Return a dict in which the dictionaries keys are mapped to new keys."""
|
|
855 |
mapped = {} |
|
856 |
for key, value in dict_.iteritems(): |
|
857 |
mapped_key = key_map[key] if key in key_map else key |
|
858 |
mapped[mapped_key] = value |
|
859 |
return mapped |
|
860 |
||
861 |
||
862 |
def subset_dict(dict_, keys): |
|
863 |
"""Return a dict that only contains a subset of keys."""
|
|
864 |
subset = partition_dict(dict_, keys)[0] |
|
865 |
return subset |
|
866 |
||
867 |
||
868 |
def diff_dict(orig, new): |
|
869 |
"""
|
|
870 |
Return a dict describing how to change orig to new. The keys
|
|
871 |
correspond to values that have changed; the value will be a list
|
|
872 |
of one or two elements. The first element of the list will be
|
|
873 |
either '+' or '-', indicating whether the key was updated or
|
|
874 |
deleted; if the key was updated, the list will contain a second
|
|
875 |
element, giving the updated value.
|
|
876 |
"""
|
|
877 |
# Figure out what keys went away
|
|
878 |
result = dict((k, ['-']) for k in set(orig.keys()) - set(new.keys())) |
|
879 |
# Compute the updates
|
|
880 |
for key, value in new.items(): |
|
881 |
if key not in orig or value != orig[key]: |
|
882 |
result[key] = ['+', value] |
|
883 |
return result |
|
884 |
||
885 |
||
886 |
def check_isinstance(obj, cls): |
|
887 |
"""Checks that obj is of type cls, and lets PyLint infer types."""
|
|
888 |
if isinstance(obj, cls): |
|
889 |
return obj |
|
890 |
raise Exception(_('Expected object of type: %s') % (str(cls))) |
|
891 |
||
892 |
||
893 |
def parse_server_string(server_str): |
|
894 |
"""
|
|
895 |
Parses the given server_string and returns a list of host and port.
|
|
896 |
If it's not a combination of host part and port, the port element
|
|
897 |
is a null string. If the input is invalid expression, return a null
|
|
898 |
list.
|
|
899 |
"""
|
|
900 |
try: |
|
901 |
# First of all, exclude pure IPv6 address (w/o port).
|
|
902 |
if netaddr.valid_ipv6(server_str): |
|
903 |
return (server_str, '') |
|
904 |
||
905 |
# Next, check if this is IPv6 address with a port number combination.
|
|
906 |
if server_str.find("]:") != -1: |
|
907 |
(address, port) = server_str.replace('[', '', 1).split(']:') |
|
908 |
return (address, port) |
|
909 |
||
910 |
# Third, check if this is a combination of an address and a port
|
|
911 |
if server_str.find(':') == -1: |
|
912 |
return (server_str, '') |
|
913 |
||
914 |
# This must be a combination of an address and a port
|
|
915 |
(address, port) = server_str.split(':') |
|
916 |
return (address, port) |
|
917 |
||
918 |
except Exception: |
|
919 |
LOG.error(_('Invalid server_string: %s'), server_str) |
|
920 |
return ('', '') |
|
921 |
||
922 |
||
923 |
def gen_uuid(): |
|
924 |
return uuid.uuid4() |
|
925 |
||
926 |
||
927 |
def is_uuid_like(val): |
|
928 |
"""For our purposes, a UUID is a string in canonical form:
|
|
929 |
||
930 |
aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa
|
|
931 |
"""
|
|
932 |
try: |
|
933 |
uuid.UUID(val) |
|
934 |
return True |
|
935 |
except (TypeError, ValueError, AttributeError): |
|
936 |
return False |
|
937 |
||
938 |
||
939 |
def bool_from_str(val): |
|
940 |
"""Convert a string representation of a bool into a bool value"""
|
|
941 |
||
942 |
if not val: |
|
943 |
return False |
|
944 |
try: |
|
945 |
return True if int(val) else False |
|
946 |
except ValueError: |
|
947 |
return val.lower() == 'true' or \ |
|
948 |
val.lower() == 'yes' or \ |
|
949 |
val.lower() == 'y' |
|
950 |
||
951 |
||
952 |
def is_valid_boolstr(val): |
|
953 |
"""Check if the provided string is a valid bool string or not. """
|
|
954 |
val = str(val).lower() |
|
955 |
return val == 'true' or val == 'false' or \ |
|
956 |
val == 'yes' or val == 'no' or \ |
|
957 |
val == 'y' or val == 'n' or \ |
|
958 |
val == '1' or val == '0' |
|
959 |
||
960 |
||
961 |
def is_valid_ipv4(address): |
|
962 |
"""valid the address strictly as per format xxx.xxx.xxx.xxx.
|
|
963 |
where xxx is a value between 0 and 255.
|
|
964 |
"""
|
|
965 |
parts = address.split(".") |
|
966 |
if len(parts) != 4: |
|
967 |
return False |
|
968 |
for item in parts: |
|
969 |
try: |
|
970 |
if not 0 <= int(item) <= 255: |
|
971 |
return False |
|
972 |
except ValueError: |
|
973 |
return False |
|
974 |
return True |
|
975 |
||
976 |
||
977 |
def is_valid_cidr(address): |
|
978 |
"""Check if the provided ipv4 or ipv6 address is a valid
|
|
979 |
CIDR address or not"""
|
|
980 |
try: |
|
981 |
# Validate the correct CIDR Address
|
|
982 |
netaddr.IPNetwork(address) |
|
983 |
except netaddr.core.AddrFormatError: |
|
984 |
return False |
|
985 |
except UnboundLocalError: |
|
986 |
# NOTE(MotoKen): work around bug in netaddr 0.7.5 (see detail in
|
|
987 |
# https://github.com/drkjam/netaddr/issues/2)
|
|
988 |
return False |
|
989 |
||
990 |
# Prior validation partially verify /xx part
|
|
991 |
# Verify it here
|
|
992 |
ip_segment = address.split('/') |
|
993 |
||
994 |
if (len(ip_segment) <= 1 or |
|
995 |
ip_segment[1] == ''): |
|
996 |
return False |
|
997 |
||
998 |
return True |
|
999 |
||
1000 |
||
1001 |
def monkey_patch(): |
|
1002 |
""" If the Flags.monkey_patch set as True,
|
|
1003 |
this function patches a decorator
|
|
1004 |
for all functions in specified modules.
|
|
1005 |
You can set decorators for each modules
|
|
1006 |
using FLAGS.monkey_patch_modules.
|
|
1007 |
The format is "Module path:Decorator function".
|
|
1008 |
Example: 'nova.api.ec2.cloud:nova.notifier.api.notify_decorator'
|
|
1009 |
||
1010 |
Parameters of the decorator is as follows.
|
|
1011 |
(See nova.notifier.api.notify_decorator)
|
|
1012 |
||
1013 |
name - name of the function
|
|
1014 |
function - object of the function
|
|
1015 |
"""
|
|
1016 |
# If FLAGS.monkey_patch is not True, this function do nothing.
|
|
1017 |
if not FLAGS.monkey_patch: |
|
1018 |
return
|
|
1019 |
# Get list of modules and decorators
|
|
1020 |
for module_and_decorator in FLAGS.monkey_patch_modules: |
|
1021 |
module, decorator_name = module_and_decorator.split(':') |
|
1022 |
# import decorator function
|
|
1023 |
decorator = importutils.import_class(decorator_name) |
|
1024 |
__import__(module) |
|
1025 |
# Retrieve module information using pyclbr
|
|
1026 |
module_data = pyclbr.readmodule_ex(module) |
|
1027 |
for key in module_data.keys(): |
|
1028 |
# set the decorator for the class methods
|
|
1029 |
if isinstance(module_data[key], pyclbr.Class): |
|
1030 |
clz = importutils.import_class("%s.%s" % (module, key)) |
|
1031 |
for method, func in inspect.getmembers(clz, inspect.ismethod): |
|
1032 |
setattr(clz, method, |
|
1033 |
decorator("%s.%s.%s" % (module, key, method), func)) |
|
1034 |
# set the decorator for the function
|
|
1035 |
if isinstance(module_data[key], pyclbr.Function): |
|
1036 |
func = importutils.import_class("%s.%s" % (module, key)) |
|
1037 |
setattr(sys.modules[module], key, |
|
1038 |
decorator("%s.%s" % (module, key), func)) |
|
1039 |
||
1040 |
||
1041 |
def convert_to_list_dict(lst, label): |
|
1042 |
"""Convert a value or list into a list of dicts"""
|
|
1043 |
if not lst: |
|
1044 |
return None |
|
1045 |
if not isinstance(lst, list): |
|
1046 |
lst = [lst] |
|
1047 |
return [{label: x} for x in lst] |
|
1048 |
||
1049 |
||
1050 |
def timefunc(func): |
|
1051 |
"""Decorator that logs how long a particular function took to execute"""
|
|
1052 |
@functools.wraps(func) |
|
1053 |
def inner(*args, **kwargs): |
|
1054 |
start_time = time.time() |
|
1055 |
try: |
|
1056 |
return func(*args, **kwargs) |
|
1057 |
finally: |
|
1058 |
total_time = time.time() - start_time |
|
1059 |
LOG.debug(_("timefunc: '%(name)s' took %(total_time).2f secs") % |
|
1060 |
dict(name=func.__name__, total_time=total_time)) |
|
1061 |
return inner |
|
1062 |
||
1063 |
||
1064 |
def generate_glance_url(): |
|
1065 |
"""Generate the URL to glance."""
|
|
1066 |
# TODO(jk0): This will eventually need to take SSL into consideration
|
|
1067 |
# when supported in glance.
|
|
1068 |
return "http://%s:%d" % (FLAGS.glance_host, FLAGS.glance_port) |
|
1069 |
||
1070 |
||
1071 |
def generate_image_url(image_ref): |
|
1072 |
"""Generate an image URL from an image_ref."""
|
|
1073 |
return "%s/images/%s" % (generate_glance_url(), image_ref) |
|
1074 |
||
1075 |
||
1076 |
@contextlib.contextmanager |
|
1077 |
def remove_path_on_error(path): |
|
1078 |
"""Protect code that wants to operate on PATH atomically.
|
|
1079 |
Any exception will cause PATH to be removed.
|
|
1080 |
"""
|
|
1081 |
try: |
|
1082 |
yield
|
|
1083 |
except Exception: |
|
1084 |
with excutils.save_and_reraise_exception(): |
|
1085 |
delete_if_exists(path) |
|
1086 |
||
1087 |
||
1088 |
def make_dev_path(dev, partition=None, base='/dev'): |
|
1089 |
"""Return a path to a particular device.
|
|
1090 |
||
1091 |
>>> make_dev_path('xvdc')
|
|
1092 |
/dev/xvdc
|
|
1093 |
||
1094 |
>>> make_dev_path('xvdc', 1)
|
|
1095 |
/dev/xvdc1
|
|
1096 |
"""
|
|
1097 |
path = os.path.join(base, dev) |
|
1098 |
if partition: |
|
1099 |
path += str(partition) |
|
1100 |
return path |
|
1101 |
||
1102 |
||
1103 |
def total_seconds(td): |
|
1104 |
"""Local total_seconds implementation for compatibility with python 2.6"""
|
|
1105 |
if hasattr(td, 'total_seconds'): |
|
1106 |
return td.total_seconds() |
|
1107 |
else: |
|
1108 |
return ((td.days * 86400 + td.seconds) * 10 ** 6 + |
|
1109 |
td.microseconds) / 10.0 ** 6 |
|
1110 |
||
1111 |
||
1112 |
def sanitize_hostname(hostname): |
|
1113 |
"""Return a hostname which conforms to RFC-952 and RFC-1123 specs."""
|
|
1114 |
if isinstance(hostname, unicode): |
|
1115 |
hostname = hostname.encode('latin-1', 'ignore') |
|
1116 |
||
1117 |
hostname = re.sub('[ _]', '-', hostname) |
|
1118 |
hostname = re.sub('[^\w.-]+', '', hostname) |
|
1119 |
hostname = hostname.lower() |
|
1120 |
hostname = hostname.strip('.-') |
|
1121 |
||
1122 |
return hostname |
|
1123 |
||
1124 |
||
1125 |
def read_cached_file(filename, cache_info, reload_func=None): |
|
1126 |
"""Read from a file if it has been modified.
|
|
1127 |
||
1128 |
:param cache_info: dictionary to hold opaque cache.
|
|
1129 |
:param reload_func: optional function to be called with data when
|
|
1130 |
file is reloaded due to a modification.
|
|
1131 |
||
1132 |
:returns: data from file
|
|
1133 |
||
1134 |
"""
|
|
1135 |
mtime = os.path.getmtime(filename) |
|
1136 |
if not cache_info or mtime != cache_info.get('mtime'): |
|
1137 |
LOG.debug(_("Reloading cached file %s") % filename) |
|
1138 |
with open(filename) as fap: |
|
1139 |
cache_info['data'] = fap.read() |
|
1140 |
cache_info['mtime'] = mtime |
|
1141 |
if reload_func: |
|
1142 |
reload_func(cache_info['data']) |
|
1143 |
return cache_info['data'] |
|
1144 |
||
1145 |
||
1146 |
def file_open(*args, **kwargs): |
|
1147 |
"""Open file
|
|
1148 |
||
1149 |
see built-in file() documentation for more details
|
|
1150 |
||
1151 |
Note: The reason this is kept in a separate module is to easily
|
|
1152 |
be able to provide a stub module that doesn't alter system
|
|
1153 |
state at all (for unit tests)
|
|
1154 |
"""
|
|
1155 |
return file(*args, **kwargs) |
|
1156 |
||
1157 |
||
1158 |
def hash_file(file_like_object): |
|
1159 |
"""Generate a hash for the contents of a file."""
|
|
1160 |
checksum = hashlib.sha1() |
|
1161 |
for chunk in iter(lambda: file_like_object.read(32768), b''): |
|
1162 |
checksum.update(chunk) |
|
1163 |
return checksum.hexdigest() |
|
1164 |
||
1165 |
||
1166 |
@contextlib.contextmanager |
|
1167 |
def temporary_mutation(obj, **kwargs): |
|
1168 |
"""Temporarily set the attr on a particular object to a given value then
|
|
1169 |
revert when finished.
|
|
1170 |
||
1171 |
One use of this is to temporarily set the read_deleted flag on a context
|
|
1172 |
object:
|
|
1173 |
||
1174 |
with temporary_mutation(context, read_deleted="yes"):
|
|
1175 |
do_something_that_needed_deleted_objects()
|
|
1176 |
"""
|
|
1177 |
NOT_PRESENT = object() |
|
1178 |
||
1179 |
old_values = {} |
|
1180 |
for attr, new_value in kwargs.items(): |
|
1181 |
old_values[attr] = getattr(obj, attr, NOT_PRESENT) |
|
1182 |
setattr(obj, attr, new_value) |
|
1183 |
||
1184 |
try: |
|
1185 |
yield
|
|
1186 |
finally: |
|
1187 |
for attr, old_value in old_values.items(): |
|
1188 |
if old_value is NOT_PRESENT: |
|
1189 |
del obj[attr] |
|
1190 |
else: |
|
1191 |
setattr(obj, attr, old_value) |
|
1192 |
||
1193 |
||
1194 |
def service_is_up(service): |
|
1195 |
"""Check whether a service is up based on last heartbeat."""
|
|
1196 |
last_heartbeat = service['updated_at'] or service['created_at'] |
|
1197 |
# Timestamps in DB are UTC.
|
|
1198 |
elapsed = total_seconds(timeutils.utcnow() - last_heartbeat) |
|
1199 |
return abs(elapsed) <= FLAGS.service_down_time |
|
1200 |
||
1201 |
||
1202 |
def generate_mac_address(): |
|
1203 |
"""Generate an Ethernet MAC address."""
|
|
1204 |
# NOTE(vish): We would prefer to use 0xfe here to ensure that linux
|
|
1205 |
# bridge mac addresses don't change, but it appears to
|
|
1206 |
# conflict with libvirt, so we use the next highest octet
|
|
1207 |
# that has the unicast and locally administered bits set
|
|
1208 |
# properly: 0xfa.
|
|
1209 |
# Discussion: https://bugs.launchpad.net/nova/+bug/921838
|
|
1210 |
mac = [0xfa, 0x16, 0x3e, |
|
1211 |
random.randint(0x00, 0x7f), |
|
1212 |
random.randint(0x00, 0xff), |
|
1213 |
random.randint(0x00, 0xff)] |
|
1214 |
return ':'.join(map(lambda x: "%02x" % x, mac)) |
|
1215 |
||
1216 |
||
1217 |
def read_file_as_root(file_path): |
|
1218 |
"""Secure helper to read file as root."""
|
|
1219 |
try: |
|
1220 |
out, _err = execute('cat', file_path, run_as_root=True) |
|
1221 |
return out |
|
1222 |
except exception.ProcessExecutionError: |
|
1223 |
raise exception.FileNotFound(file_path=file_path) |
|
1224 |
||
1225 |
||
1226 |
@contextlib.contextmanager |
|
1227 |
def temporary_chown(path, owner_uid=None): |
|
1228 |
"""Temporarily chown a path.
|
|
1229 |
||
1230 |
:params owner_uid: UID of temporary owner (defaults to current user)
|
|
1231 |
"""
|
|
1232 |
if owner_uid is None: |
|
1233 |
owner_uid = os.getuid() |
|
1234 |
||
1235 |
orig_uid = os.stat(path).st_uid |
|
1236 |
||
1237 |
if orig_uid != owner_uid: |
|
1238 |
execute('chown', owner_uid, path, run_as_root=True) |
|
1239 |
try: |
|
1240 |
yield
|
|
1241 |
finally: |
|
1242 |
if orig_uid != owner_uid: |
|
1243 |
execute('chown', orig_uid, path, run_as_root=True) |
|
1244 |
||
1245 |
||
1246 |
@contextlib.contextmanager |
|
1247 |
def tempdir(**kwargs): |
|
1248 |
tmpdir = tempfile.mkdtemp(**kwargs) |
|
1249 |
try: |
|
1250 |
yield tmpdir |
|
1251 |
finally: |
|
1252 |
try: |
|
1253 |
shutil.rmtree(tmpdir) |
|
1254 |
except OSError, e: |
|
1255 |
LOG.error(_('Could not remove tmpdir: %s'), str(e)) |
|
1256 |
||
1257 |
||
1258 |
def strcmp_const_time(s1, s2): |
|
1259 |
"""Constant-time string comparison.
|
|
1260 |
||
1261 |
:params s1: the first string
|
|
1262 |
:params s2: the second string
|
|
1263 |
||
1264 |
:return: True if the strings are equal.
|
|
1265 |
||
1266 |
This function takes two strings and compares them. It is intended to be
|
|
1267 |
used when doing a comparison for authentication purposes to help guard
|
|
1268 |
against timing attacks.
|
|
1269 |
"""
|
|
1270 |
if len(s1) != len(s2): |
|
1271 |
return False |
|
1272 |
result = 0 |
|
1273 |
for (a, b) in zip(s1, s2): |
|
1274 |
result |= ord(a) ^ ord(b) |
|
1275 |
return result == 0 |
|
1276 |
||
1277 |
||
1278 |
def walk_class_hierarchy(clazz, encountered=None): |
|
1279 |
"""Walk class hierarchy, yielding most derived classes first"""
|
|
1280 |
if not encountered: |
|
1281 |
encountered = [] |
|
1282 |
for subclass in clazz.__subclasses__(): |
|
1283 |
if subclass not in encountered: |
|
1284 |
encountered.append(subclass) |
|
1285 |
# drill down to leaves first
|
|
1286 |
for subsubclass in walk_class_hierarchy(subclass, encountered): |
|
1287 |
yield subsubclass |
|
1288 |
yield subclass |
|
1289 |
||
1290 |
||
1291 |
class UndoManager(object): |
|
1292 |
"""Provides a mechanism to facilitate rolling back a series of actions
|
|
1293 |
when an exception is raised.
|
|
1294 |
"""
|
|
1295 |
def __init__(self): |
|
1296 |
self.undo_stack = [] |
|
1297 |
||
1298 |
def undo_with(self, undo_func): |
|
1299 |
self.undo_stack.append(undo_func) |
|
1300 |
||
1301 |
def _rollback(self): |
|
1302 |
for undo_func in reversed(self.undo_stack): |
|
1303 |
undo_func() |
|
1304 |
||
1305 |
def rollback_and_reraise(self, msg=None, **kwargs): |
|
1306 |
"""Rollback a series of actions then re-raise the exception.
|
|
1307 |
||
1308 |
.. note:: (sirp) This should only be called within an
|
|
1309 |
exception handler.
|
|
1310 |
"""
|
|
1311 |
with excutils.save_and_reraise_exception(): |
|
1312 |
if msg: |
|
1313 |
LOG.exception(msg, **kwargs) |
|
1314 |
||
1315 |
self._rollback() |
|
1316 |
||
1317 |
||
1318 |
def ensure_tree(path): |
|
1319 |
"""Create a directory (and any ancestor directories required)
|
|
1320 |
||
1321 |
:param path: Directory to create
|
|
1322 |
"""
|
|
1323 |
try: |
|
1324 |
os.makedirs(path) |
|
1325 |
except OSError as exc: |
|
1326 |
if exc.errno == errno.EEXIST: |
|
1327 |
if not os.path.isdir(path): |
|
1328 |
raise
|
|
1329 |
else: |
|
1330 |
raise
|
|
1331 |
||
1332 |
||
1333 |
def last_bytes(file_like_object, num): |
|
1334 |
"""Return num bytes from the end of the file, and remaining byte count.
|
|
1335 |
||
1336 |
:param file_like_object: The file to read
|
|
1337 |
:param num: The number of bytes to return
|
|
1338 |
||
1339 |
:returns (data, remaining)
|
|
1340 |
"""
|
|
1341 |
||
1342 |
try: |
|
1343 |
file_like_object.seek(-num, os.SEEK_END) |
|
1344 |
except IOError, e: |
|
1345 |
if e.errno == 22: |
|
1346 |
file_like_object.seek(0, os.SEEK_SET) |
|
1347 |
else: |
|
1348 |
raise
|
|
1349 |
||
1350 |
remaining = file_like_object.tell() |
|
1351 |
return (file_like_object.read(), remaining) |