# Copyright 2017 Red Hat, 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. import logging import re from mistral_lib import actions from tripleo_common.actions import base from tripleo_common import exception LOG = logging.getLogger(__name__) class GetDpdkNicsNumaInfoAction(base.TripleOAction): """Gets the DPDK NICs with MTU for NUMA nodes. Find the DPDK interface names from the network config and translate it to phsical interface names using the introspection data. And then find the NUMA node associated with the DPDK interface and the MTU value. :param network_configs: network config list :param inspect_data: introspection data :param mtu_default: mtu default value for NICs :return: DPDK NICs NUMA nodes info """ def __init__(self, network_configs, inspect_data, mtu_default=1500): super(GetDpdkNicsNumaInfoAction, self).__init__() self.network_configs = network_configs self.inspect_data = inspect_data self.mtu_default = mtu_default # TODO(jpalanis): Expose this utility from os-net-config to sort # active nics def _natural_sort_key(self, s): nsre = re.compile('([0-9]+)') return [int(text) if text.isdigit() else text for text in re.split(nsre, s)] # TODO(jpalanis): Expose this utility from os-net-config to sort # active nics def _is_embedded_nic(self, nic): if (nic.startswith('em') or nic.startswith('eth') or nic.startswith('eno')): return True return False # TODO(jpalanis): Expose this utility from os-net-config to sort # active nics def _ordered_nics(self, interfaces): embedded_nics = [] nics = [] for iface in interfaces: nic = iface.get('name', '') if self._is_embedded_nic(nic): embedded_nics.append(nic) else: nics.append(nic) active_nics = (sorted( embedded_nics, key=self._natural_sort_key) + sorted(nics, key=self._natural_sort_key)) return active_nics # Gets numa node id for physical NIC name def find_numa_node_id(self, numa_nics, nic_name): for nic_info in numa_nics: if nic_info.get('name', '') == nic_name: return nic_info.get('numa_node', None) return None # Get physical interface name for NIC name def get_physical_iface_name(self, ordered_nics, nic_name): if nic_name.startswith('nic'): # Nic numbering, find the actual interface name nic_number = int(nic_name.replace('nic', '')) if nic_number > 0: iface_name = ordered_nics[nic_number - 1] return iface_name return nic_name # Gets dpdk interfaces and mtu info for dpdk config # Default mtu(recommended 1500) is used if no MTU is set for DPDK NIC def get_dpdk_interfaces(self, dpdk_objs): mtu = self.mtu_default dpdk_ifaces = [] for dpdk_obj in dpdk_objs: obj_type = dpdk_obj.get('type') mtu = dpdk_obj.get('mtu', self.mtu_default) if obj_type == 'ovs_dpdk_port': # Member interfaces of ovs_dpdk_port dpdk_ifaces.extend(dpdk_obj.get('members', [])) elif obj_type == 'ovs_dpdk_bond': # ovs_dpdk_bond will have multiple ovs_dpdk_ports for bond_member in dpdk_obj.get('members', []): if bond_member.get('type') == 'ovs_dpdk_port': dpdk_ifaces.extend(bond_member.get('members', [])) return (dpdk_ifaces, mtu) def run(self, context): interfaces = self.inspect_data.get('inventory', {}).get('interfaces', []) # Checks whether inventory interfaces information is not available # in introspection data. if not interfaces: msg = 'Introspection data does not have inventory.interfaces' return actions.Result(error=msg) numa_nics = self.inspect_data.get('numa_topology', {}).get('nics', []) # Checks whether numa topology nics information is not available # in introspection data. if not numa_nics: msg = 'Introspection data does not have numa_topology.nics' return actions.Result(error=msg) active_interfaces = [iface for iface in interfaces if iface.get('has_carrier', False)] # Checks whether active interfaces are not available if not active_interfaces: msg = 'Unable to determine active interfaces (has_carrier)' return actions.Result(error=msg) dpdk_nics_numa_info = [] ordered_nics = self._ordered_nics(active_interfaces) # Gets DPDK network config and parses to get DPDK NICs # with mtu and numa node id for config in self.network_configs: if config.get('type', '') == 'ovs_user_bridge': members = config.get('members', []) dpdk_ifaces, mtu = self.get_dpdk_interfaces(members) for dpdk_iface in dpdk_ifaces: name = dpdk_iface.get('name', '') phy_name = self.get_physical_iface_name( ordered_nics, name) node = self.find_numa_node_id(numa_nics, phy_name) if not node: msg = ('Unable to determine NUMA node for ' 'DPDK NIC: %s' % phy_name) return actions.Result(error=msg) dpdk_nic_info = {'name': phy_name, 'numa_node': node, 'mtu': mtu} dpdk_nics_numa_info.append(dpdk_nic_info) return dpdk_nics_numa_info class GetDpdkCoreListAction(base.TripleOAction): """Gets the DPDK PMD Core List. With input as the number of physical cores for each NUMA node, find the right logical CPUs to be allocated along with its siblings for the PMD core list. :param inspect_data: introspection data :param numa_nodes_cores_count: physical cores count for each NUMA :return: DPDK Core List """ def __init__(self, inspect_data, numa_nodes_cores_count): super(GetDpdkCoreListAction, self).__init__() self.inspect_data = inspect_data self.numa_nodes_cores_count = numa_nodes_cores_count def run(self, context): dpdk_core_list = [] numa_cpus_info = self.inspect_data.get('numa_topology', {}).get('cpus', []) # Checks whether numa topology cpus information is not available # in introspection data. if not numa_cpus_info: msg = 'Introspection data does not have numa_topology.cpus' return actions.Result(error=msg) # Checks whether CPU physical cores count for each NUMA nodes is # not available if not self.numa_nodes_cores_count: msg = ('CPU physical cores count for each NUMA nodes ' 'is not available') return actions.Result(error=msg) numa_nodes_threads = {} # Creates list for all available threads in each NUMA node for cpu in numa_cpus_info: if not cpu['numa_node'] in numa_nodes_threads: numa_nodes_threads[cpu['numa_node']] = [] numa_nodes_threads[cpu['numa_node']].extend(cpu['thread_siblings']) for node_cores_count in self.numa_nodes_cores_count: node = self.numa_nodes_cores_count.index(node_cores_count) # Gets least thread in NUMA node numa_node_min = min(numa_nodes_threads[node]) cores_count = node_cores_count for cpu in numa_cpus_info: if cpu['numa_node'] == node: # Adds threads from core which is not having least thread if numa_node_min not in cpu['thread_siblings']: dpdk_core_list.extend(cpu['thread_siblings']) cores_count -= 1 if cores_count == 0: break return ','.join([str(thread) for thread in dpdk_core_list]) class GetHostCpusListAction(base.TripleOAction): """Gets the Host CPUs List. CPU threads from first physical core is allocated for host processes on each NUMA nodes. :param inspect_data: introspection data :return: Host CPUs List """ def __init__(self, inspect_data): super(GetHostCpusListAction, self).__init__() self.inspect_data = inspect_data def run(self, context): host_cpus_list = [] numa_cpus_info = self.inspect_data.get('numa_topology', {}).get('cpus', []) # Checks whether numa topology cpus information is not available # in introspection data. if not numa_cpus_info: msg = 'Introspection data does not have numa_topology.cpus' return actions.Result(error=msg) numa_nodes_threads = {} # Creates a list for all available threads in each NUMA nodes for cpu in numa_cpus_info: if not cpu['numa_node'] in numa_nodes_threads: numa_nodes_threads[cpu['numa_node']] = [] numa_nodes_threads[cpu['numa_node']].extend( cpu['thread_siblings']) for numa_node in numa_nodes_threads.keys(): node = int(numa_node) # Gets least thread in NUMA node numa_node_min = min(numa_nodes_threads[numa_node]) for cpu in numa_cpus_info: if cpu['numa_node'] == node: # Adds threads from core which is having least thread if numa_node_min in cpu['thread_siblings']: host_cpus_list.extend(cpu['thread_siblings']) break return ','.join([str(thread) for thread in host_cpus_list]) class GetDpdkSocketMemoryAction(base.TripleOAction): """Gets the DPDK Socket Memory List. For NUMA node with DPDK nic, socket memory is calculated based on MTU, Overhead and Packet size in buffer. For NUMA node without DPDK nic, minimum socket memory is assigned (recommended 1GB) :param dpdk_nics_numa_info: DPDK nics numa info :param numa_nodes: list of numa nodes :param overhead: overhead value :param packet_size_in_buffer: packet size in buffer :param minimum_socket_memory: minimum socket memory :return: DPDK Socket Memory List """ def __init__(self, dpdk_nics_numa_info, numa_nodes, overhead, packet_size_in_buffer, minimum_socket_memory=1024): super(GetDpdkSocketMemoryAction, self).__init__() self.dpdk_nics_numa_info = dpdk_nics_numa_info self.numa_nodes = numa_nodes self.overhead = overhead self.packet_size_in_buffer = packet_size_in_buffer self.minimum_socket_memory = minimum_socket_memory # Calculates socket memory for a NUMA node def calculate_node_socket_memory( self, numa_node, dpdk_nics_numa_info, overhead, packet_size_in_buffer, minimum_socket_memory): distinct_mtu_per_node = [] socket_memory = 0 # For DPDK numa node for nics_info in dpdk_nics_numa_info: if (numa_node == nics_info['numa_node'] and not nics_info['mtu'] in distinct_mtu_per_node): distinct_mtu_per_node.append(nics_info['mtu']) socket_memory += (((nics_info['mtu'] + overhead) * packet_size_in_buffer) / (1024 * 1024)) # For Non DPDK numa node if socket_memory == 0: socket_memory = minimum_socket_memory # For DPDK numa node else: socket_memory += 500 socket_memory_in_gb = int(socket_memory / 1024) if socket_memory % 1024 > 0: socket_memory_in_gb += 1 return (socket_memory_in_gb * 1024) def run(self, context): dpdk_socket_memory_list = [] for node in self.numa_nodes: socket_mem = self.calculate_node_socket_memory( node, self.dpdk_nics_numa_info, self.overhead, self.packet_size_in_buffer, self.minimum_socket_memory) dpdk_socket_memory_list.append(socket_mem) return ','.join([str(sm) for sm in dpdk_socket_memory_list]) class ConvertNumberToRangeListAction(base.TripleOAction): """Converts number list into range list :param num_list: comma delimited number list as string :return: comma delimited range list as string """ def __init__(self, num_list): super(ConvertNumberToRangeListAction, self).__init__() self.num_list = num_list # converts number list into range list. # here input parameter and return value as list # example: [12, 13, 14, 17] into ["12-14", "17"] def convert_number_to_range_list(self, num_list): num_list.sort() range_list = [] range_min = num_list[0] for num in num_list: next_val = num + 1 if next_val not in num_list: if range_min != num: range_list.append(str(range_min) + '-' + str(num)) else: range_list.append(str(range_min)) next_index = num_list.index(num) + 1 if next_index < len(num_list): range_min = num_list[next_index] # here, range_list is a list of strings return range_list def run(self, context): try: if not self.num_list: err_msg = ("Input param 'num_list' is blank.") raise exception.DeriveParamsError(err_msg) try: # splitting a string (comma delimited list) into # list of numbers # example: "12,13,14,17" string into [12,13,14,17] num_list = [int(num.strip(' ')) for num in self.num_list.split(",")] except ValueError as exc: err_msg = ("Invalid number in input param " "'num_list': %s" % exc) raise exception.DeriveParamsError(err_msg) range_list = self.convert_number_to_range_list(num_list) except exception.DeriveParamsError as err: LOG.error('Derive Params Error: %s', err) return actions.Result(error=str(err)) # converts into comma delimited range list as string return ','.join(range_list) class ConvertRangeToNumberListAction(base.TripleOAction): """Converts range list to integer list :param range_list: comma delimited range list as string / list :return: comma delimited number list as string """ def __init__(self, range_list): super(ConvertRangeToNumberListAction, self).__init__() self.range_list = range_list # converts range list into number list # here input parameter and return value as list # example: ["12-14", "^13", "17"] into [12, 14, 17] def convert_range_to_number_list(self, range_list): num_list = [] exclude_num_list = [] try: for val in range_list: val = val.strip(' ') if '^' in val: exclude_num_list.append(int(val[1:])) elif '-' in val: split_list = val.split("-") range_min = int(split_list[0]) range_max = int(split_list[1]) num_list.extend(range(range_min, (range_max + 1))) else: num_list.append(int(val)) except ValueError as exc: err_msg = ("Invalid number in input param " "'range_list': %s" % exc) raise exception.DeriveParamsError(err_msg) # here, num_list is a list of integers return [num for num in num_list if num not in exclude_num_list] def run(self, context): try: if not self.range_list: err_msg = ("Input param 'range_list' is blank.") raise exception.DeriveParamsError(err_msg) range_list = self.range_list # converts into python list if range_list is not list type if not isinstance(range_list, list): range_list = self.range_list.split(",") num_list = self.convert_range_to_number_list(range_list) except exception.DeriveParamsError as err: LOG.error('Derive Params Error: %s', err) return actions.Result(error=str(err)) # converts into comma delimited number list as string return ','.join([str(num) for num in num_list])