98e3bed120143ef00fe4670f14a941d5b6dfb410
[openstack-build/neutron-build.git] / neutron / common / utils.py
1 # Copyright 2011, VMware, Inc.
2 # All Rights Reserved.
3 #
4 #    Licensed under the Apache License, Version 2.0 (the "License"); you may
5 #    not use this file except in compliance with the License. You may obtain
6 #    a copy of the License at
7 #
8 #         http://www.apache.org/licenses/LICENSE-2.0
9 #
10 #    Unless required by applicable law or agreed to in writing, software
11 #    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 #    License for the specific language governing permissions and limitations
14 #    under the License.
15 #
16 # Borrowed from nova code base, more utilities will be added/borrowed as and
17 # when needed.
18
19 """Utilities and helper functions."""
20
21 import collections
22 import datetime
23 import decimal
24 import errno
25 import functools
26 import hashlib
27 import multiprocessing
28 import netaddr
29 import os
30 import random
31 import signal
32 import socket
33 import sys
34 import tempfile
35 import uuid
36
37 import debtcollector
38 from eventlet.green import subprocess
39 from oslo_concurrency import lockutils
40 from oslo_config import cfg
41 from oslo_log import log as logging
42 from oslo_utils import excutils
43 from oslo_utils import importutils
44 from oslo_utils import reflection
45 import six
46 from stevedore import driver
47
48 from neutron._i18n import _, _LE
49 from neutron.common import constants as n_const
50
51 TIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
52 LOG = logging.getLogger(__name__)
53 SYNCHRONIZED_PREFIX = 'neutron-'
54
55 synchronized = lockutils.synchronized_with_prefix(SYNCHRONIZED_PREFIX)
56
57
58 class cache_method_results(object):
59     """This decorator is intended for object methods only."""
60
61     def __init__(self, func):
62         self.func = func
63         functools.update_wrapper(self, func)
64         self._first_call = True
65         self._not_cached = object()
66
67     def _get_from_cache(self, target_self, *args, **kwargs):
68         target_self_cls_name = reflection.get_class_name(target_self,
69                                                          fully_qualified=False)
70         func_name = "%(module)s.%(class)s.%(func_name)s" % {
71             'module': target_self.__module__,
72             'class': target_self_cls_name,
73             'func_name': self.func.__name__,
74         }
75         key = (func_name,) + args
76         if kwargs:
77             key += dict2tuple(kwargs)
78         try:
79             item = target_self._cache.get(key, self._not_cached)
80         except TypeError:
81             LOG.debug("Method %(func_name)s cannot be cached due to "
82                       "unhashable parameters: args: %(args)s, kwargs: "
83                       "%(kwargs)s",
84                       {'func_name': func_name,
85                        'args': args,
86                        'kwargs': kwargs})
87             return self.func(target_self, *args, **kwargs)
88
89         if item is self._not_cached:
90             item = self.func(target_self, *args, **kwargs)
91             target_self._cache.set(key, item, None)
92
93         return item
94
95     def __call__(self, target_self, *args, **kwargs):
96         target_self_cls_name = reflection.get_class_name(target_self,
97                                                          fully_qualified=False)
98         if not hasattr(target_self, '_cache'):
99             raise NotImplementedError(
100                 _("Instance of class %(module)s.%(class)s must contain _cache "
101                   "attribute") % {
102                     'module': target_self.__module__,
103                     'class': target_self_cls_name})
104         if not target_self._cache:
105             if self._first_call:
106                 LOG.debug("Instance of class %(module)s.%(class)s doesn't "
107                           "contain attribute _cache therefore results "
108                           "cannot be cached for %(func_name)s.",
109                           {'module': target_self.__module__,
110                            'class': target_self_cls_name,
111                            'func_name': self.func.__name__})
112                 self._first_call = False
113             return self.func(target_self, *args, **kwargs)
114         return self._get_from_cache(target_self, *args, **kwargs)
115
116     def __get__(self, obj, objtype):
117         return functools.partial(self.__call__, obj)
118
119
120 @debtcollector.removals.remove(message="This will removed in the N cycle.")
121 def read_cached_file(filename, cache_info, reload_func=None):
122     """Read from a file if it has been modified.
123
124     :param cache_info: dictionary to hold opaque cache.
125     :param reload_func: optional function to be called with data when
126                         file is reloaded due to a modification.
127
128     :returns: data from file
129
130     """
131     mtime = os.path.getmtime(filename)
132     if not cache_info or mtime != cache_info.get('mtime'):
133         LOG.debug("Reloading cached file %s", filename)
134         with open(filename) as fap:
135             cache_info['data'] = fap.read()
136         cache_info['mtime'] = mtime
137         if reload_func:
138             reload_func(cache_info['data'])
139     return cache_info['data']
140
141
142 @debtcollector.removals.remove(message="This will removed in the N cycle.")
143 def find_config_file(options, config_file):
144     """Return the first config file found.
145
146     We search for the paste config file in the following order:
147     * If --config-file option is used, use that
148     * Search for the configuration files via common cfg directories
149     :retval Full path to config file, or None if no config file found
150     """
151     fix_path = lambda p: os.path.abspath(os.path.expanduser(p))
152     if options.get('config_file'):
153         if os.path.exists(options['config_file']):
154             return fix_path(options['config_file'])
155
156     dir_to_common = os.path.dirname(os.path.abspath(__file__))
157     root = os.path.join(dir_to_common, '..', '..', '..', '..')
158     # Handle standard directory search for the config file
159     config_file_dirs = [fix_path(os.path.join(os.getcwd(), 'etc')),
160                         fix_path(os.path.join('~', '.neutron-venv', 'etc',
161                                               'neutron')),
162                         fix_path('~'),
163                         os.path.join(cfg.CONF.state_path, 'etc'),
164                         os.path.join(cfg.CONF.state_path, 'etc', 'neutron'),
165                         fix_path(os.path.join('~', '.local',
166                                               'etc', 'neutron')),
167                         '/usr/etc/neutron',
168                         '/usr/local/etc/neutron',
169                         '/etc/neutron/',
170                         '/etc']
171
172     if 'plugin' in options:
173         config_file_dirs = [
174             os.path.join(x, 'neutron', 'plugins', options['plugin'])
175             for x in config_file_dirs
176         ]
177
178     if os.path.exists(os.path.join(root, 'plugins')):
179         plugins = [fix_path(os.path.join(root, 'plugins', p, 'etc'))
180                    for p in os.listdir(os.path.join(root, 'plugins'))]
181         plugins = [p for p in plugins if os.path.isdir(p)]
182         config_file_dirs.extend(plugins)
183
184     for cfg_dir in config_file_dirs:
185         cfg_file = os.path.join(cfg_dir, config_file)
186         if os.path.exists(cfg_file):
187             return cfg_file
188
189
190 def ensure_dir(dir_path):
191     """Ensure a directory with 755 permissions mode."""
192     try:
193         os.makedirs(dir_path, 0o755)
194     except OSError as e:
195         # If the directory already existed, don't raise the error.
196         if e.errno != errno.EEXIST:
197             raise
198
199
200 def _subprocess_setup():
201     # Python installs a SIGPIPE handler by default. This is usually not what
202     # non-Python subprocesses expect.
203     signal.signal(signal.SIGPIPE, signal.SIG_DFL)
204
205
206 def subprocess_popen(args, stdin=None, stdout=None, stderr=None, shell=False,
207                      env=None, preexec_fn=_subprocess_setup, close_fds=True):
208
209     return subprocess.Popen(args, shell=shell, stdin=stdin, stdout=stdout,
210                             stderr=stderr, preexec_fn=preexec_fn,
211                             close_fds=close_fds, env=env)
212
213
214 def parse_mappings(mapping_list, unique_values=True):
215     """Parse a list of mapping strings into a dictionary.
216
217     :param mapping_list: a list of strings of the form '<key>:<value>'
218     :param unique_values: values must be unique if True
219     :returns: a dict mapping keys to values
220     """
221     mappings = {}
222     for mapping in mapping_list:
223         mapping = mapping.strip()
224         if not mapping:
225             continue
226         split_result = mapping.split(':')
227         if len(split_result) != 2:
228             raise ValueError(_("Invalid mapping: '%s'") % mapping)
229         key = split_result[0].strip()
230         if not key:
231             raise ValueError(_("Missing key in mapping: '%s'") % mapping)
232         value = split_result[1].strip()
233         if not value:
234             raise ValueError(_("Missing value in mapping: '%s'") % mapping)
235         if key in mappings:
236             raise ValueError(_("Key %(key)s in mapping: '%(mapping)s' not "
237                                "unique") % {'key': key, 'mapping': mapping})
238         if unique_values and value in mappings.values():
239             raise ValueError(_("Value %(value)s in mapping: '%(mapping)s' "
240                                "not unique") % {'value': value,
241                                                 'mapping': mapping})
242         mappings[key] = value
243     return mappings
244
245
246 def get_hostname():
247     return socket.gethostname()
248
249
250 def get_first_host_ip(net, ip_version):
251     return str(netaddr.IPAddress(net.first + 1, ip_version))
252
253
254 def compare_elements(a, b):
255     """Compare elements if a and b have same elements.
256
257     This method doesn't consider ordering
258     """
259     if a is None:
260         a = []
261     if b is None:
262         b = []
263     return set(a) == set(b)
264
265
266 def safe_sort_key(value):
267     """Return value hash or build one for dictionaries."""
268     if isinstance(value, collections.Mapping):
269         return sorted(value.items())
270     return value
271
272
273 def dict2str(dic):
274     return ','.join("%s=%s" % (key, val)
275                     for key, val in sorted(six.iteritems(dic)))
276
277
278 def str2dict(string):
279     res_dict = {}
280     for keyvalue in string.split(','):
281         (key, value) = keyvalue.split('=', 1)
282         res_dict[key] = value
283     return res_dict
284
285
286 def dict2tuple(d):
287     items = list(d.items())
288     items.sort()
289     return tuple(items)
290
291
292 def diff_list_of_dict(old_list, new_list):
293     new_set = set([dict2str(l) for l in new_list])
294     old_set = set([dict2str(l) for l in old_list])
295     added = new_set - old_set
296     removed = old_set - new_set
297     return [str2dict(a) for a in added], [str2dict(r) for r in removed]
298
299
300 def is_extension_supported(plugin, ext_alias):
301     return ext_alias in getattr(
302         plugin, "supported_extension_aliases", [])
303
304
305 def log_opt_values(log):
306     cfg.CONF.log_opt_values(log, logging.DEBUG)
307
308
309 def get_random_mac(base_mac):
310     mac = [int(base_mac[0], 16), int(base_mac[1], 16),
311            int(base_mac[2], 16), random.randint(0x00, 0xff),
312            random.randint(0x00, 0xff), random.randint(0x00, 0xff)]
313     if base_mac[3] != '00':
314         mac[3] = int(base_mac[3], 16)
315     return ':'.join(["%02x" % x for x in mac])
316
317
318 def get_random_string(length):
319     """Get a random hex string of the specified length.
320
321     based on Cinder library
322       cinder/transfer/api.py
323     """
324     rndstr = ""
325     random.seed(datetime.datetime.now().microsecond)
326     while len(rndstr) < length:
327         base_str = str(random.random()).encode('utf-8')
328         rndstr += hashlib.sha224(base_str).hexdigest()
329
330     return rndstr[0:length]
331
332
333 def get_dhcp_agent_device_id(network_id, host):
334     # Split host so as to always use only the hostname and
335     # not the domain name. This will guarantee consistency
336     # whether a local hostname or an fqdn is passed in.
337     local_hostname = host.split('.')[0]
338     host_uuid = uuid.uuid5(uuid.NAMESPACE_DNS, str(local_hostname))
339     return 'dhcp%s-%s' % (host_uuid, network_id)
340
341
342 def cpu_count():
343     try:
344         return multiprocessing.cpu_count()
345     except NotImplementedError:
346         return 1
347
348
349 class exception_logger(object):
350     """Wrap a function and log raised exception
351
352     :param logger: the logger to log the exception default is LOG.exception
353
354     :returns: origin value if no exception raised; re-raise the exception if
355               any occurred
356
357     """
358     def __init__(self, logger=None):
359         self.logger = logger
360
361     def __call__(self, func):
362         if self.logger is None:
363             LOG = logging.getLogger(func.__module__)
364             self.logger = LOG.exception
365
366         def call(*args, **kwargs):
367             try:
368                 return func(*args, **kwargs)
369             except Exception as e:
370                 with excutils.save_and_reraise_exception():
371                     self.logger(e)
372         return call
373
374
375 def get_other_dvr_serviced_device_owners():
376     """Return device_owner names for ports that should be serviced by DVR
377
378     This doesn't return DEVICE_OWNER_COMPUTE_PREFIX since it is a
379     prefix, not a complete device_owner name, so should be handled
380     separately (see is_dvr_serviced() below)
381     """
382     return [n_const.DEVICE_OWNER_LOADBALANCER,
383             n_const.DEVICE_OWNER_LOADBALANCERV2,
384             n_const.DEVICE_OWNER_DHCP]
385
386
387 def is_dvr_serviced(device_owner):
388     """Check if the port need to be serviced by DVR
389
390     Helper function to check the device owners of the
391     ports in the compute and service node to make sure
392     if they are required for DVR or any service directly or
393     indirectly associated with DVR.
394     """
395     return (device_owner.startswith(n_const.DEVICE_OWNER_COMPUTE_PREFIX) or
396             device_owner in get_other_dvr_serviced_device_owners())
397
398
399 @debtcollector.removals.remove(message="This will removed in the N cycle.")
400 def get_keystone_url(conf):
401     if conf.auth_uri:
402         auth_uri = conf.auth_uri.rstrip('/')
403     else:
404         auth_uri = ('%(protocol)s://%(host)s:%(port)s' %
405             {'protocol': conf.auth_protocol,
406              'host': conf.auth_host,
407              'port': conf.auth_port})
408     # NOTE(ihrachys): all existing consumers assume version 2.0
409     return '%s/v2.0/' % auth_uri
410
411
412 def ip_to_cidr(ip, prefix=None):
413     """Convert an ip with no prefix to cidr notation
414
415     :param ip: An ipv4 or ipv6 address.  Convertable to netaddr.IPNetwork.
416     :param prefix: Optional prefix.  If None, the default 32 will be used for
417         ipv4 and 128 for ipv6.
418     """
419     net = netaddr.IPNetwork(ip)
420     if prefix is not None:
421         # Can't pass ip and prefix separately.  Must concatenate strings.
422         net = netaddr.IPNetwork(str(net.ip) + '/' + str(prefix))
423     return str(net)
424
425
426 def fixed_ip_cidrs(fixed_ips):
427     """Create a list of a port's fixed IPs in cidr notation.
428
429     :param fixed_ips: A neutron port's fixed_ips dictionary
430     """
431     return [ip_to_cidr(fixed_ip['ip_address'], fixed_ip.get('prefixlen'))
432             for fixed_ip in fixed_ips]
433
434
435 def is_cidr_host(cidr):
436     """Determines if the cidr passed in represents a single host network
437
438     :param cidr: Either an ipv4 or ipv6 cidr.
439     :returns: True if the cidr is /32 for ipv4 or /128 for ipv6.
440     :raises ValueError: raises if cidr does not contain a '/'.  This disallows
441         plain IP addresses specifically to avoid ambiguity.
442     """
443     if '/' not in str(cidr):
444         raise ValueError("cidr doesn't contain a '/'")
445     net = netaddr.IPNetwork(cidr)
446     if net.version == 4:
447         return net.prefixlen == n_const.IPv4_BITS
448     return net.prefixlen == n_const.IPv6_BITS
449
450
451 def ip_version_from_int(ip_version_int):
452     if ip_version_int == 4:
453         return n_const.IPv4
454     if ip_version_int == 6:
455         return n_const.IPv6
456     raise ValueError(_('Illegal IP version number'))
457
458
459 def is_port_trusted(port):
460     """Used to determine if port can be trusted not to attack network.
461
462     Trust is currently based on the device_owner field starting with 'network:'
463     since we restrict who can use that in the default policy.json file.
464     """
465     return port['device_owner'].startswith(n_const.DEVICE_OWNER_NETWORK_PREFIX)
466
467
468 class DelayedStringRenderer(object):
469     """Takes a callable and its args and calls when __str__ is called
470
471     Useful for when an argument to a logging statement is expensive to
472     create. This will prevent the callable from being called if it's
473     never converted to a string.
474     """
475
476     def __init__(self, function, *args, **kwargs):
477         self.function = function
478         self.args = args
479         self.kwargs = kwargs
480
481     def __str__(self):
482         return str(self.function(*self.args, **self.kwargs))
483
484
485 def camelize(s):
486     return ''.join(s.replace('_', ' ').title().split())
487
488
489 def round_val(val):
490     # we rely on decimal module since it behaves consistently across Python
491     # versions (2.x vs. 3.x)
492     return int(decimal.Decimal(val).quantize(decimal.Decimal('1'),
493                                              rounding=decimal.ROUND_HALF_UP))
494
495
496 def replace_file(file_name, data, file_mode=0o644):
497     """Replaces the contents of file_name with data in a safe manner.
498
499     First write to a temp file and then rename. Since POSIX renames are
500     atomic, the file is unlikely to be corrupted by competing writes.
501
502     We create the tempfile on the same device to ensure that it can be renamed.
503     """
504
505     base_dir = os.path.dirname(os.path.abspath(file_name))
506     with tempfile.NamedTemporaryFile('w+',
507                                      dir=base_dir,
508                                      delete=False) as tmp_file:
509         tmp_file.write(data)
510     os.chmod(tmp_file.name, file_mode)
511     os.rename(tmp_file.name, file_name)
512
513
514 def load_class_by_alias_or_classname(namespace, name):
515     """Load class using stevedore alias or the class name
516     :param namespace: namespace where the alias is defined
517     :param name: alias or class name of the class to be loaded
518     :returns class if calls can be loaded
519     :raises ImportError if class cannot be loaded
520     """
521
522     if not name:
523         LOG.error(_LE("Alias or class name is not set"))
524         raise ImportError(_("Class not found."))
525     try:
526         # Try to resolve class by alias
527         mgr = driver.DriverManager(namespace, name)
528         class_to_load = mgr.driver
529     except RuntimeError:
530         e1_info = sys.exc_info()
531         # Fallback to class name
532         try:
533             class_to_load = importutils.import_class(name)
534         except (ImportError, ValueError):
535             LOG.error(_LE("Error loading class by alias"),
536                       exc_info=e1_info)
537             LOG.error(_LE("Error loading class by class name"),
538                       exc_info=True)
539             raise ImportError(_("Class not found."))
540     return class_to_load
541
542
543 def safe_decode_utf8(s):
544     if six.PY3 and isinstance(s, bytes):
545         return s.decode('utf-8', 'surrogateescape')
546     return s