# Copyright 2010 United States Government as represented by the # Administrator of the National Aeronautics and Space Administration. # Copyright 2014 SoftLayer Technologies, Inc. # Copyright 2015 Mirantis, Inc # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ System-level utilities and helper functions. """ import errno from functools import reduce try: from eventlet import sleep except ImportError: from time import sleep from eventlet.green import socket import functools import os import platform import re import subprocess import sys import uuid import copy from OpenSSL import crypto from oslo.config import cfg from oslo.log import log as logging from oslo.utils import encodeutils from oslo.utils import excutils import six from webob import exc from escalator.common import exception from escalator import i18n CONF = cfg.CONF LOG = logging.getLogger(__name__) _ = i18n._ _LE = i18n._LE ESCALATOR_TEST_SOCKET_FD_STR = 'ESCALATOR_TEST_SOCKET_FD' def chunkreadable(iter, chunk_size=65536): """ Wrap a readable iterator with a reader yielding chunks of a preferred size, otherwise leave iterator unchanged. :param iter: an iter which may also be readable :param chunk_size: maximum size of chunk """ return chunkiter(iter, chunk_size) if hasattr(iter, 'read') else iter def chunkiter(fp, chunk_size=65536): """ Return an iterator to a file-like obj which yields fixed size chunks :param fp: a file-like object :param chunk_size: maximum size of chunk """ while True: chunk = fp.read(chunk_size) if chunk: yield chunk else: break def cooperative_iter(iter): """ Return an iterator which schedules after each iteration. This can prevent eventlet thread starvation. :param iter: an iterator to wrap """ try: for chunk in iter: sleep(0) yield chunk except Exception as err: with excutils.save_and_reraise_exception(): msg = _LE("Error: cooperative_iter exception %s") % err LOG.error(msg) def cooperative_read(fd): """ Wrap a file descriptor's read with a partial function which schedules after each read. This can prevent eventlet thread starvation. :param fd: a file descriptor to wrap """ def readfn(*args): result = fd.read(*args) sleep(0) return result return readfn MAX_COOP_READER_BUFFER_SIZE = 134217728 # 128M seems like a sane buffer limit class CooperativeReader(object): """ An eventlet thread friendly class for reading in image data. When accessing data either through the iterator or the read method we perform a sleep to allow a co-operative yield. When there is more than one image being uploaded/downloaded this prevents eventlet thread starvation, ie allows all threads to be scheduled periodically rather than having the same thread be continuously active. """ def __init__(self, fd): """ :param fd: Underlying image file object """ self.fd = fd self.iterator = None # NOTE(markwash): if the underlying supports read(), overwrite the # default iterator-based implementation with cooperative_read which # is more straightforward if hasattr(fd, 'read'): self.read = cooperative_read(fd) else: self.iterator = None self.buffer = '' self.position = 0 def read(self, length=None): """Return the requested amount of bytes, fetching the next chunk of the underlying iterator when needed. This is replaced with cooperative_read in __init__ if the underlying fd already supports read(). """ if length is None: if len(self.buffer) - self.position > 0: # if no length specified but some data exists in buffer, # return that data and clear the buffer result = self.buffer[self.position:] self.buffer = '' self.position = 0 return str(result) else: # otherwise read the next chunk from the underlying iterator # and return it as a whole. Reset the buffer, as subsequent # calls may specify the length try: if self.iterator is None: self.iterator = self.__iter__() return self.iterator.next() except StopIteration: return '' finally: self.buffer = '' self.position = 0 else: result = bytearray() while len(result) < length: if self.position < len(self.buffer): to_read = length - len(result) chunk = self.buffer[self.position:self.position + to_read] result.extend(chunk) # This check is here to prevent potential OOM issues if # this code is called with unreasonably high values of read # size. Currently it is only called from the HTTP clients # of Glance backend stores, which use httplib for data # streaming, which has readsize hardcoded to 8K, so this # check should never fire. Regardless it still worths to # make the check, as the code may be reused somewhere else. if len(result) >= MAX_COOP_READER_BUFFER_SIZE: raise exception.LimitExceeded() self.position += len(chunk) else: try: if self.iterator is None: self.iterator = self.__iter__() self.buffer = self.iterator.next() self.position = 0 except StopIteration: self.buffer = '' self.position = 0 return str(result) return str(result) def __iter__(self): return cooperative_iter(self.fd.__iter__()) class LimitingReader(object): """ Reader designed to fail when reading image data past the configured allowable amount. """ def __init__(self, data, limit): """ :param data: Underlying image data object :param limit: maximum number of bytes the reader should allow """ self.data = data self.limit = limit self.bytes_read = 0 def __iter__(self): for chunk in self.data: self.bytes_read += len(chunk) if self.bytes_read > self.limit: raise exception.ImageSizeLimitExceeded() else: yield chunk def read(self, i): result = self.data.read(i) self.bytes_read += len(result) if self.bytes_read > self.limit: raise exception.ImageSizeLimitExceeded() return result def get_dict_meta(response): result = {} for key, value in response.json.items(): result[key] = value return result def create_mashup_dict(image_meta): """ Returns a dictionary-like mashup of the image core properties and the image custom properties from given image metadata. :param image_meta: metadata of image with core and custom properties """ def get_items(): for key, value in six.iteritems(image_meta): if isinstance(value, dict): for subkey, subvalue in six.iteritems( create_mashup_dict(value)): if subkey not in image_meta: yield subkey, subvalue else: yield key, value return dict(get_items()) def safe_mkdirs(path): try: os.makedirs(path) except OSError as e: if e.errno != errno.EEXIST: raise def safe_remove(path): try: os.remove(path) except OSError as e: if e.errno != errno.ENOENT: raise class PrettyTable(object): """Creates an ASCII art table for use in bin/escalator """ def __init__(self): self.columns = [] def add_column(self, width, label="", just='l'): """Add a column to the table :param width: number of characters wide the column should be :param label: column heading :param just: justification for the column, 'l' for left, 'r' for right """ self.columns.append((width, label, just)) def make_header(self): label_parts = [] break_parts = [] for width, label, _ in self.columns: # NOTE(sirp): headers are always left justified label_part = self._clip_and_justify(label, width, 'l') label_parts.append(label_part) break_part = '-' * width break_parts.append(break_part) label_line = ' '.join(label_parts) break_line = ' '.join(break_parts) return '\n'.join([label_line, break_line]) def make_row(self, *args): row = args row_parts = [] for data, (width, _, just) in zip(row, self.columns): row_part = self._clip_and_justify(data, width, just) row_parts.append(row_part) row_line = ' '.join(row_parts) return row_line @staticmethod def _clip_and_justify(data, width, just): # clip field to column width clipped_data = str(data)[:width] if just == 'r': # right justify justified = clipped_data.rjust(width) else: # left justify justified = clipped_data.ljust(width) return justified def get_terminal_size(): def _get_terminal_size_posix(): import fcntl import struct import termios height_width = None try: height_width = struct.unpack('hh', fcntl.ioctl(sys.stderr.fileno(), termios.TIOCGWINSZ, struct.pack( 'HH', 0, 0))) except Exception: pass if not height_width: try: p = subprocess.Popen(['stty', 'size'], shell=False, stdout=subprocess.PIPE, stderr=open(os.devnull, 'w')) result = p.communicate() if p.returncode == 0: return tuple(int(x) for x in result[0].split()) except Exception: pass return height_width def _get_terminal_size_win32(): try: from ctypes import create_string_buffer from ctypes import windll handle = windll.kernel32.GetStdHandle(-12) csbi = create_string_buffer(22) res = windll.kernel32.GetConsoleScreenBufferInfo(handle, csbi) except Exception: return None if res: import struct unpack_tmp = struct.unpack("hhhhHhhhhhh", csbi.raw) (bufx, bufy, curx, cury, wattr, left, top, right, bottom, maxx, maxy) = unpack_tmp height = bottom - top + 1 width = right - left + 1 return (height, width) else: return None def _get_terminal_size_unknownOS(): raise NotImplementedError func = {'posix': _get_terminal_size_posix, 'win32': _get_terminal_size_win32} height_width = func.get(platform.os.name, _get_terminal_size_unknownOS)() if height_width is None: raise exception.Invalid() for i in height_width: if not isinstance(i, int) or i <= 0: raise exception.Invalid() return height_width[0], height_width[1] def mutating(func): """Decorator to enforce read-only logic""" @functools.wraps(func) def wrapped(self, req, *args, **kwargs): if req.context.read_only: msg = "Read-only access" LOG.debug(msg) raise exc.HTTPForbidden(msg, request=req, content_type="text/plain") return func(self, req, *args, **kwargs) return wrapped def setup_remote_pydev_debug(host, port): error_msg = _LE('Error setting up the debug environment. Verify that the' ' option pydev_worker_debug_host is pointing to a valid ' 'hostname or IP on which a pydev server is listening on' ' the port indicated by pydev_worker_debug_port.') try: try: from pydev import pydevd except ImportError: import pydevd pydevd.settrace(host, port=port, stdoutToServer=True, stderrToServer=True) return True except Exception: with excutils.save_and_reraise_exception(): LOG.exception(error_msg) def validate_key_cert(key_file, cert_file): try: error_key_name = "private key" error_filename = key_file with open(key_file, 'r') as keyfile: key_str = keyfile.read() key = crypto.load_privatekey(crypto.FILETYPE_PEM, key_str) error_key_name = "certificate" error_filename = cert_file with open(cert_file, 'r') as certfile: cert_str = certfile.read() cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert_str) except IOError as ioe: raise RuntimeError(_("There is a problem with your %(error_key_name)s " "%(error_filename)s. Please verify it." " Error: %(ioe)s") % {'error_key_name': error_key_name, 'error_filename': error_filename, 'ioe': ioe}) except crypto.Error as ce: raise RuntimeError(_("There is a problem with your %(error_key_name)s " "%(error_filename)s. Please verify it. OpenSSL" " error: %(ce)s") % {'error_key_name': error_key_name, 'error_filename': error_filename, 'ce': ce}) try: data = str(uuid.uuid4()) digest = CONF.digest_algorithm if digest == 'sha1': LOG.warn('The FIPS (FEDERAL INFORMATION PROCESSING STANDARDS)' ' state that the SHA-1 is not suitable for' ' general-purpose digital signature applications (as' ' specified in FIPS 186-3) that require 112 bits of' ' security. The default value is sha1 in Kilo for a' ' smooth upgrade process, and it will be updated' ' with sha256 in next release(L).') out = crypto.sign(key, data, digest) crypto.verify(cert, out, data, digest) except crypto.Error as ce: raise RuntimeError(_("There is a problem with your key pair. " "Please verify that cert %(cert_file)s and " "key %(key_file)s belong together. OpenSSL " "error %(ce)s") % {'cert_file': cert_file, 'key_file': key_file, 'ce': ce}) def get_test_suite_socket(): global ESCALATOR_TEST_SOCKET_FD_STR if ESCALATOR_TEST_SOCKET_FD_STR in os.environ: fd = int(os.environ[ESCALATOR_TEST_SOCKET_FD_STR]) sock = socket.fromfd(fd, socket.AF_INET, socket.SOCK_STREAM) sock = socket.SocketType(_sock=sock) sock.listen(CONF.backlog) del os.environ[ESCALATOR_TEST_SOCKET_FD_STR] os.close(fd) return sock return None def is_uuid_like(val): """Returns validation of a value as a UUID. For our purposes, a UUID is a canonical form string: aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa """ try: return str(uuid.UUID(val)) == val except (TypeError, ValueError, AttributeError): return False def exception_to_str(exc): try: error = six.text_type(exc) except UnicodeError: try: error = str(exc) except UnicodeError: error = ("Caught '%(exception)s' exception." % {"exception": exc.__class__.__name__}) return encodeutils.safe_encode(error, errors='ignore') try: REGEX_4BYTE_UNICODE = re.compile(u'[\U00010000-\U0010ffff]') except re.error: # UCS-2 build case REGEX_4BYTE_UNICODE = re.compile(u'[\uD800-\uDBFF][\uDC00-\uDFFF]') def no_4byte_params(f): """ Checks that no 4 byte unicode characters are allowed in dicts' keys/values and string's parameters """ def wrapper(*args, **kwargs): def _is_match(some_str): return (isinstance(some_str, unicode) and REGEX_4BYTE_UNICODE.findall(some_str) != []) def _check_dict(data_dict): # a dict of dicts has to be checked recursively for key, value in data_dict.iteritems(): if isinstance(value, dict): _check_dict(value) else: if _is_match(key): msg = _("Property names can't contain 4 byte unicode.") raise exception.Invalid(msg) if _is_match(value): msg = (_("%s can't contain 4 byte unicode characters.") % key.title()) raise exception.Invalid(msg) for data_dict in [arg for arg in args if isinstance(arg, dict)]: _check_dict(data_dict) # now check args for str values for arg in args: if _is_match(arg): msg = _("Param values can't contain 4 byte unicode.") raise exception.Invalid(msg) # check kwargs as well, as params are passed as kwargs via # registry calls _check_dict(kwargs) return f(*args, **kwargs) return wrapper def stash_conf_values(): """ Make a copy of some of the current global CONF's settings. Allows determining if any of these values have changed when the config is reloaded. """ conf = {} conf['bind_host'] = CONF.bind_host conf['bind_port'] = CONF.bind_port conf['tcp_keepidle'] = CONF.cert_file conf['backlog'] = CONF.backlog conf['key_file'] = CONF.key_file conf['cert_file'] = CONF.cert_file return conf def validate_ip_format(ip_str): ''' valid ip_str format = '10.43.178.9' invalid ip_str format : '123. 233.42.12', spaces existed in field '3234.23.453.353', out of range '-2.23.24.234', negative number in field '1.2.3.4d', letter in field '10.43.1789', invalid format ''' if not ip_str: msg = (_("No ip given when check ip")) LOG.error(msg) raise exc.HTTPBadRequest(msg, content_type="text/plain") valid_fromat = False if ip_str.count('.') == 3 and all(num.isdigit() and 0 <= int( num) < 256 for num in ip_str.rstrip().split('.')): valid_fromat = True if not valid_fromat: msg = (_("%s invalid ip format!") % ip_str) LOG.error(msg) raise exc.HTTPBadRequest(msg, content_type="text/plain") def valid_cidr(cidr): if not cidr: msg = (_("No CIDR given.")) LOG.error(msg) raise exc.HTTPBadRequest(explanation=msg) cidr_division = cidr.split('/') if (len(cidr_division) != 2 or not cidr_division[0] or not cidr_division[1]): msg = (_("CIDR format error.")) LOG.error(msg) raise exc.HTTPBadRequest(explanation=msg) netmask_err_msg = (_("CIDR netmask error, " "it should be a integer between 0-32.")) try: netmask_cidr = int(cidr_division[1]) except ValueError: LOG.warn(netmask_err_msg) raise exc.HTTPBadRequest(explanation=netmask_err_msg) if (netmask_cidr < 0 and netmask_cidr > 32): LOG.warn(netmask_err_msg) raise exc.HTTPBadRequest(explanation=netmask_err_msg) validate_ip_format(cidr_division[0]) def ip_into_int(ip): """ Switch ip string to decimalism integer.. :param ip: ip string :return: decimalism integer """ return reduce(lambda x, y: (x << 8) + y, map(int, ip.split('.'))) def int_into_ip(num): s = [] for i in range(4): s.append(str(num % 256)) num /= 256 return '.'.join(s[::-1]) def is_ip_in_cidr(ip, cidr): """ Check ip is in cidr :param ip: Ip will be checked, like:192.168.1.2. :param cidr: Ip range,like:192.168.0.0/24. :return: If ip in cidr, return True, else return False. """ if not ip: msg = "Error, ip is empty" raise exc.HTTPBadRequest(explanation=msg) if not cidr: msg = "Error, CIDR is empty" raise exc.HTTPBadRequest(explanation=msg) network = cidr.split('/') mask = ~(2**(32 - int(network[1])) - 1) return (ip_into_int(ip) & mask) == (ip_into_int(network[0]) & mask) def is_ip_in_ranges(ip, ip_ranges): """ Check ip is in range : ip: Ip will be checked, like:192.168.1.2. : ip_ranges : Ip ranges, like: [{'start':'192.168.0.10', 'end':'192.168.0.20'} {'start':'192.168.0.50', 'end':'192.168.0.60'}] :return: If ip in ip_ranges, return True, else return False. """ if not ip: msg = "Error, ip is empty" raise exc.HTTPBadRequest(explanation=msg) if not ip_ranges: return True for ip_range in ip_ranges: start_ip_int = ip_into_int(ip_range['start']) end_ip_int = ip_into_int(ip_range['end']) ip_int = ip_into_int(ip) if ip_int >= start_ip_int and ip_int <= end_ip_int: return True return False def merge_ip_ranges(ip_ranges): if not ip_ranges: return ip_ranges sort_ranges_by_start_ip = {} for ip_range in ip_ranges: start_ip_int = ip_into_int(ip_range['start']) sort_ranges_by_start_ip.update({str(start_ip_int): ip_range}) sort_ranges = [sort_ranges_by_start_ip[key] for key in sorted(sort_ranges_by_start_ip.keys())] last_range_end_ip = None merged_ip_ranges = [] for ip_range in sort_ranges: if last_range_end_ip is None: last_range_end_ip = ip_range['end'] merged_ip_ranges.append(ip_range) continue else: last_range_end_ip_int = ip_into_int(last_range_end_ip) ip_range_start_ip_int = ip_into_int(ip_range['start']) if (last_range_end_ip_int + 1) == ip_range_start_ip_int: merged_ip_ranges[-1]['end'] = ip_range['end'] else: merged_ip_ranges.append(ip_range) return merged_ip_ranges def _split_ip_ranges(ip_ranges): ip_ranges_start = set() ip_ranges_end = set() if not ip_ranges: return (ip_ranges_start, ip_ranges_end) for ip_range in ip_ranges: ip_ranges_start.add(ip_range['start']) ip_ranges_end.add(ip_range['end']) return (ip_ranges_start, ip_ranges_end) # [{'start':'192.168.0.10', 'end':'192.168.0.20'}, # {'start':'192.168.0.21', 'end':'192.168.0.22'}] and # [{'start':'192.168.0.10', 'end':'192.168.0.22'}] is equal here def is_ip_ranges_equal(ip_ranges1, ip_ranges2): if not ip_ranges1 and not ip_ranges2: return True if ((ip_ranges1 and not ip_ranges2) or (ip_ranges2 and not ip_ranges1)): return False ip_ranges_1 = copy.deepcopy(ip_ranges1) ip_ranges_2 = copy.deepcopy(ip_ranges2) merged_ip_ranges1 = merge_ip_ranges(ip_ranges_1) merged_ip_ranges2 = merge_ip_ranges(ip_ranges_2) ip_ranges1_start, ip_ranges1_end = _split_ip_ranges(merged_ip_ranges1) ip_ranges2_start, ip_ranges2_end = _split_ip_ranges(merged_ip_ranges2) if (ip_ranges1_start == ip_ranges2_start and ip_ranges1_end == ip_ranges2_end): return True else: return False def get_dvs_interfaces(host_interfaces): dvs_interfaces = [] if not isinstance(host_interfaces, list): host_interfaces = eval(host_interfaces) for interface in host_interfaces: if not isinstance(interface, dict): interface = eval(interface) if ('vswitch_type' in interface and interface['vswitch_type'] == 'dvs'): dvs_interfaces.append(interface) return dvs_interfaces def get_clc_pci_info(pci_info): clc_pci = [] flag1 = 'Intel Corporation Coleto Creek PCIe Endpoint' flag2 = '8086:0435' for pci in pci_info: if flag1 in pci or flag2 in pci: clc_pci.append(pci.split()[0]) return clc_pci def cpu_str_to_list(spec): """Parse a CPU set specification. :param spec: cpu set string eg "1-4,^3,6" Each element in the list is either a single CPU number, a range of CPU numbers, or a caret followed by a CPU number to be excluded from a previous range. :returns: a set of CPU indexes """ cpusets = [] if not spec: return cpusets cpuset_ids = set() cpuset_reject_ids = set() for rule in spec.split(','): rule = rule.strip() # Handle multi ',' if len(rule) < 1: continue # Note the count limit in the .split() call range_parts = rule.split('-', 1) if len(range_parts) > 1: # So, this was a range; start by converting the parts to ints try: start, end = [int(p.strip()) for p in range_parts] except ValueError: raise exception.Invalid(_("Invalid range expression %r") % rule) # Make sure it's a valid range if start > end: raise exception.Invalid(_("Invalid range expression %r") % rule) # Add available CPU ids to set cpuset_ids |= set(range(start, end + 1)) elif rule[0] == '^': # Not a range, the rule is an exclusion rule; convert to int try: cpuset_reject_ids.add(int(rule[1:].strip())) except ValueError: raise exception.Invalid(_("Invalid exclusion " "expression %r") % rule) else: # OK, a single CPU to include; convert to int try: cpuset_ids.add(int(rule)) except ValueError: raise exception.Invalid(_("Invalid inclusion " "expression %r") % rule) # Use sets to handle the exclusion rules for us cpuset_ids -= cpuset_reject_ids cpusets = list(cpuset_ids) cpusets.sort() return cpusets def cpu_list_to_str(cpu_list): """Parse a CPU list to string. :param cpu_list: eg "[1,2,3,4,6,7]" :returns: a string of CPU ranges, eg 1-4,6,7 """ spec = '' if not cpu_list: return spec cpu_list.sort() count = 0 group_cpus = [] tmp_cpus = [] for cpu in cpu_list: if count == 0: init = cpu tmp_cpus.append(cpu) else: if cpu == (init + count): tmp_cpus.append(cpu) else: group_cpus.append(tmp_cpus) tmp_cpus = [] count = 0 init = cpu tmp_cpus.append(cpu) count += 1 group_cpus.append(tmp_cpus) for group in group_cpus: if len(group) > 2: group_spec = ("%s-%s" % (group[0], group[0]+len(group)-1)) else: group_str = [str(num) for num in group] group_spec = ','.join(group_str) if spec: spec += ',' + group_spec else: spec = group_spec return spec def simple_subprocess_call(cmd): return_code = subprocess.call(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) return return_code def translate_quotation_marks_for_shell(orig_str): translated_str = '' quotation_marks = '"' quotation_marks_count = orig_str.count(quotation_marks) if quotation_marks_count > 0: replace_marks = '\\"' translated_str = orig_str.replace(quotation_marks, replace_marks) else: translated_str = orig_str return translated_str def translate_marks_4_sed_command(ori_str): translated_str = ori_str translated_marks = { '/': '\/', '.': '\.', '"': '\\"'} for translated_mark in translated_marks: if translated_str.count(translated_mark): translated_str = translated_str.\ replace(translated_mark, translated_marks[translated_mark]) return translated_str