1 # Copyright 2011, VMware, Inc.
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
8 # http://www.apache.org/licenses/LICENSE-2.0
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
16 # Borrowed from nova code base, more utilities will be added/borrowed as and
19 """Utilities and helper functions."""
27 import multiprocessing
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
46 from stevedore import driver
48 from neutron._i18n import _, _LE
49 from neutron.common import constants as n_const
51 TIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
52 LOG = logging.getLogger(__name__)
53 SYNCHRONIZED_PREFIX = 'neutron-'
55 synchronized = lockutils.synchronized_with_prefix(SYNCHRONIZED_PREFIX)
58 class cache_method_results(object):
59 """This decorator is intended for object methods only."""
61 def __init__(self, func):
63 functools.update_wrapper(self, func)
64 self._first_call = True
65 self._not_cached = object()
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__,
75 key = (func_name,) + args
77 key += dict2tuple(kwargs)
79 item = target_self._cache.get(key, self._not_cached)
81 LOG.debug("Method %(func_name)s cannot be cached due to "
82 "unhashable parameters: args: %(args)s, kwargs: "
84 {'func_name': func_name,
87 return self.func(target_self, *args, **kwargs)
89 if item is self._not_cached:
90 item = self.func(target_self, *args, **kwargs)
91 target_self._cache.set(key, item, None)
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 "
102 'module': target_self.__module__,
103 'class': target_self_cls_name})
104 if not target_self._cache:
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)
116 def __get__(self, obj, objtype):
117 return functools.partial(self.__call__, obj)
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.
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.
128 :returns: data from file
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
138 reload_func(cache_info['data'])
139 return cache_info['data']
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.
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
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'])
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',
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',
168 '/usr/local/etc/neutron',
172 if 'plugin' in options:
174 os.path.join(x, 'neutron', 'plugins', options['plugin'])
175 for x in config_file_dirs
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)
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):
190 def ensure_dir(dir_path):
191 """Ensure a directory with 755 permissions mode."""
193 os.makedirs(dir_path, 0o755)
195 # If the directory already existed, don't raise the error.
196 if e.errno != errno.EEXIST:
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)
206 def subprocess_popen(args, stdin=None, stdout=None, stderr=None, shell=False,
207 env=None, preexec_fn=_subprocess_setup, close_fds=True):
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)
214 def parse_mappings(mapping_list, unique_values=True):
215 """Parse a list of mapping strings into a dictionary.
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
222 for mapping in mapping_list:
223 mapping = mapping.strip()
226 split_result = mapping.split(':')
227 if len(split_result) != 2:
228 raise ValueError(_("Invalid mapping: '%s'") % mapping)
229 key = split_result[0].strip()
231 raise ValueError(_("Missing key in mapping: '%s'") % mapping)
232 value = split_result[1].strip()
234 raise ValueError(_("Missing value in mapping: '%s'") % mapping)
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,
242 mappings[key] = value
247 return socket.gethostname()
250 def get_first_host_ip(net, ip_version):
251 return str(netaddr.IPAddress(net.first + 1, ip_version))
254 def compare_elements(a, b):
255 """Compare elements if a and b have same elements.
257 This method doesn't consider ordering
263 return set(a) == set(b)
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())
274 return ','.join("%s=%s" % (key, val)
275 for key, val in sorted(six.iteritems(dic)))
278 def str2dict(string):
280 for keyvalue in string.split(','):
281 (key, value) = keyvalue.split('=', 1)
282 res_dict[key] = value
287 items = list(d.items())
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]
300 def is_extension_supported(plugin, ext_alias):
301 return ext_alias in getattr(
302 plugin, "supported_extension_aliases", [])
305 def log_opt_values(log):
306 cfg.CONF.log_opt_values(log, logging.DEBUG)
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])
318 def get_random_string(length):
319 """Get a random hex string of the specified length.
321 based on Cinder library
322 cinder/transfer/api.py
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()
330 return rndstr[0:length]
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)
344 return multiprocessing.cpu_count()
345 except NotImplementedError:
349 class exception_logger(object):
350 """Wrap a function and log raised exception
352 :param logger: the logger to log the exception default is LOG.exception
354 :returns: origin value if no exception raised; re-raise the exception if
358 def __init__(self, logger=None):
361 def __call__(self, func):
362 if self.logger is None:
363 LOG = logging.getLogger(func.__module__)
364 self.logger = LOG.exception
366 def call(*args, **kwargs):
368 return func(*args, **kwargs)
369 except Exception as e:
370 with excutils.save_and_reraise_exception():
375 def get_other_dvr_serviced_device_owners():
376 """Return device_owner names for ports that should be serviced by DVR
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)
382 return [n_const.DEVICE_OWNER_LOADBALANCER,
383 n_const.DEVICE_OWNER_LOADBALANCERV2,
384 n_const.DEVICE_OWNER_DHCP]
387 def is_dvr_serviced(device_owner):
388 """Check if the port need to be serviced by DVR
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.
395 return (device_owner.startswith(n_const.DEVICE_OWNER_COMPUTE_PREFIX) or
396 device_owner in get_other_dvr_serviced_device_owners())
399 @debtcollector.removals.remove(message="This will removed in the N cycle.")
400 def get_keystone_url(conf):
402 auth_uri = conf.auth_uri.rstrip('/')
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
412 def ip_to_cidr(ip, prefix=None):
413 """Convert an ip with no prefix to cidr notation
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.
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))
426 def fixed_ip_cidrs(fixed_ips):
427 """Create a list of a port's fixed IPs in cidr notation.
429 :param fixed_ips: A neutron port's fixed_ips dictionary
431 return [ip_to_cidr(fixed_ip['ip_address'], fixed_ip.get('prefixlen'))
432 for fixed_ip in fixed_ips]
435 def is_cidr_host(cidr):
436 """Determines if the cidr passed in represents a single host network
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.
443 if '/' not in str(cidr):
444 raise ValueError("cidr doesn't contain a '/'")
445 net = netaddr.IPNetwork(cidr)
447 return net.prefixlen == n_const.IPv4_BITS
448 return net.prefixlen == n_const.IPv6_BITS
451 def ip_version_from_int(ip_version_int):
452 if ip_version_int == 4:
454 if ip_version_int == 6:
456 raise ValueError(_('Illegal IP version number'))
459 def is_port_trusted(port):
460 """Used to determine if port can be trusted not to attack network.
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.
465 return port['device_owner'].startswith(n_const.DEVICE_OWNER_NETWORK_PREFIX)
468 class DelayedStringRenderer(object):
469 """Takes a callable and its args and calls when __str__ is called
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.
476 def __init__(self, function, *args, **kwargs):
477 self.function = function
482 return str(self.function(*self.args, **self.kwargs))
486 return ''.join(s.replace('_', ' ').title().split())
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))
496 def replace_file(file_name, data, file_mode=0o644):
497 """Replaces the contents of file_name with data in a safe manner.
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.
502 We create the tempfile on the same device to ensure that it can be renamed.
505 base_dir = os.path.dirname(os.path.abspath(file_name))
506 with tempfile.NamedTemporaryFile('w+',
508 delete=False) as tmp_file:
510 os.chmod(tmp_file.name, file_mode)
511 os.rename(tmp_file.name, file_name)
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
523 LOG.error(_LE("Alias or class name is not set"))
524 raise ImportError(_("Class not found."))
526 # Try to resolve class by alias
527 mgr = driver.DriverManager(namespace, name)
528 class_to_load = mgr.driver
530 e1_info = sys.exc_info()
531 # Fallback to class name
533 class_to_load = importutils.import_class(name)
534 except (ImportError, ValueError):
535 LOG.error(_LE("Error loading class by alias"),
537 LOG.error(_LE("Error loading class by class name"),
539 raise ImportError(_("Class not found."))
543 def safe_decode_utf8(s):
544 if six.PY3 and isinstance(s, bytes):
545 return s.decode('utf-8', 'surrogateescape')