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

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, {'"': '&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)