~ubuntu-branches/ubuntu/quantal/nova/quantal-proposed

« back to all changes in this revision

Viewing changes to .pc/CVE-2013-1664.patch/nova/utils.py

  • Committer: Package Import Robot
  • Author(s): James Page
  • Date: 2013-03-22 12:40:07 UTC
  • Revision ID: package-import@ubuntu.com-20130322124007-yulmow8qdfbxsigv
Tags: 2012.2.3-0ubuntu2
* Re-sync with latest security updates.
* SECURITY UPDATE: fix denial of service via fixed IPs when using extensions
  - debian/patches/CVE-2013-1838.patch: add explicit quota for fixed IP
  - CVE-2013-1838
* SECURITY UPDATE: fix VNC token validation
  - debian/patches/CVE-2013-0335.patch: force console auth service to flush
    all tokens associated with an instance when it is deleted
  - CVE-2013-0335
* SECURITY UPDATE: fix denial of service
  - CVE-2013-1664.patch: Add a new utils.safe_minidom_parse_string function
    and update external API facing Nova modules to use it
  - CVE-2013-1664

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
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, {'"': '&quot;', "'": '&apos;'})
 
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)