A Python library for code common to TripleO CLI and TripleO UI.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

466 lines
18KB

  1. # Copyright 2017 Red Hat, 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. import logging
  16. import math
  17. import re
  18. from mistral_lib import actions
  19. from tripleo_common.actions import base
  20. from tripleo_common import exception
  21. LOG = logging.getLogger(__name__)
  22. class GetDpdkNicsNumaInfoAction(base.TripleOAction):
  23. """Gets the DPDK NICs with MTU for NUMA nodes.
  24. Find the DPDK interface names from the network config and
  25. translate it to phsical interface names using the introspection
  26. data. And then find the NUMA node associated with the DPDK
  27. interface and the MTU value.
  28. :param network_configs: network config list
  29. :param inspect_data: introspection data
  30. :param mtu_default: mtu default value for NICs
  31. :return: DPDK NICs NUMA nodes info
  32. """
  33. def __init__(self, network_configs, inspect_data, mtu_default=1500):
  34. super(GetDpdkNicsNumaInfoAction, self).__init__()
  35. self.network_configs = network_configs
  36. self.inspect_data = inspect_data
  37. self.mtu_default = mtu_default
  38. # TODO(jpalanis): Expose this utility from os-net-config to sort
  39. # active nics
  40. def _natural_sort_key(self, s):
  41. nsre = re.compile('([0-9]+)')
  42. return [int(text) if text.isdigit() else text
  43. for text in re.split(nsre, s)]
  44. # TODO(jpalanis): Expose this utility from os-net-config to sort
  45. # active nics
  46. def _is_embedded_nic(self, nic):
  47. if (nic.startswith('em') or nic.startswith('eth') or
  48. nic.startswith('eno')):
  49. return True
  50. return False
  51. # TODO(jpalanis): Expose this utility from os-net-config to sort
  52. # active nics
  53. def _ordered_nics(self, interfaces):
  54. embedded_nics = []
  55. nics = []
  56. for iface in interfaces:
  57. nic = iface.get('name', '')
  58. if self._is_embedded_nic(nic):
  59. embedded_nics.append(nic)
  60. else:
  61. nics.append(nic)
  62. active_nics = (sorted(
  63. embedded_nics, key=self._natural_sort_key) +
  64. sorted(nics, key=self._natural_sort_key))
  65. return active_nics
  66. # Gets numa node id for physical NIC name
  67. def find_numa_node_id(self, numa_nics, nic_name):
  68. for nic_info in numa_nics:
  69. if nic_info.get('name', '') == nic_name:
  70. return nic_info.get('numa_node', None)
  71. return None
  72. # Get physical interface name for NIC name
  73. def get_physical_iface_name(self, ordered_nics, nic_name):
  74. if nic_name.startswith('nic'):
  75. # Nic numbering, find the actual interface name
  76. nic_number = int(nic_name.replace('nic', ''))
  77. if nic_number > 0:
  78. iface_name = ordered_nics[nic_number - 1]
  79. return iface_name
  80. return nic_name
  81. # Gets dpdk interfaces and mtu info for dpdk config
  82. # Default mtu(recommended 1500) is used if no MTU is set for DPDK NIC
  83. def get_dpdk_interfaces(self, dpdk_objs):
  84. mtu = self.mtu_default
  85. dpdk_ifaces = []
  86. for dpdk_obj in dpdk_objs:
  87. obj_type = dpdk_obj.get('type')
  88. mtu = dpdk_obj.get('mtu', self.mtu_default)
  89. if obj_type == 'ovs_dpdk_port':
  90. # Member interfaces of ovs_dpdk_port
  91. dpdk_ifaces.extend(dpdk_obj.get('members', []))
  92. elif obj_type == 'ovs_dpdk_bond':
  93. # ovs_dpdk_bond will have multiple ovs_dpdk_ports
  94. for bond_member in dpdk_obj.get('members', []):
  95. if bond_member.get('type') == 'ovs_dpdk_port':
  96. dpdk_ifaces.extend(bond_member.get('members', []))
  97. return (dpdk_ifaces, mtu)
  98. def run(self, context):
  99. interfaces = self.inspect_data.get('inventory',
  100. {}).get('interfaces', [])
  101. # Checks whether inventory interfaces information is not available
  102. # in introspection data.
  103. if not interfaces:
  104. msg = 'Introspection data does not have inventory.interfaces'
  105. return actions.Result(error=msg)
  106. numa_nics = self.inspect_data.get('numa_topology',
  107. {}).get('nics', [])
  108. # Checks whether numa topology nics information is not available
  109. # in introspection data.
  110. if not numa_nics:
  111. msg = 'Introspection data does not have numa_topology.nics'
  112. return actions.Result(error=msg)
  113. active_interfaces = [iface for iface in interfaces
  114. if iface.get('has_carrier', False)]
  115. # Checks whether active interfaces are not available
  116. if not active_interfaces:
  117. msg = 'Unable to determine active interfaces (has_carrier)'
  118. return actions.Result(error=msg)
  119. dpdk_nics_numa_info = []
  120. ordered_nics = self._ordered_nics(active_interfaces)
  121. # Gets DPDK network config and parses to get DPDK NICs
  122. # with mtu and numa node id
  123. for config in self.network_configs:
  124. if config.get('type', '') == 'ovs_user_bridge':
  125. bridge_name = config.get('name', '')
  126. addresses = config.get('addresses', [])
  127. members = config.get('members', [])
  128. dpdk_ifaces, mtu = self.get_dpdk_interfaces(members)
  129. for dpdk_iface in dpdk_ifaces:
  130. type = dpdk_iface.get('type', '')
  131. if type == 'sriov_vf':
  132. name = dpdk_iface.get('device', '')
  133. else:
  134. name = dpdk_iface.get('name', '')
  135. phy_name = self.get_physical_iface_name(
  136. ordered_nics, name)
  137. node = self.find_numa_node_id(numa_nics, phy_name)
  138. if node is None:
  139. msg = ('Unable to determine NUMA node for '
  140. 'DPDK NIC: %s' % phy_name)
  141. return actions.Result(error=msg)
  142. dpdk_nic_info = {'name': phy_name,
  143. 'numa_node': node,
  144. 'mtu': mtu,
  145. 'bridge_name': bridge_name,
  146. 'addresses': addresses}
  147. dpdk_nics_numa_info.append(dpdk_nic_info)
  148. return dpdk_nics_numa_info
  149. class GetDpdkCoreListAction(base.TripleOAction):
  150. """Gets the DPDK PMD Core List.
  151. With input as the number of physical cores for each NUMA node,
  152. find the right logical CPUs to be allocated along with its
  153. siblings for the PMD core list.
  154. :param inspect_data: introspection data
  155. :param numa_nodes_cores_count: physical cores count for each NUMA
  156. :return: DPDK Core List
  157. """
  158. def __init__(self, inspect_data, numa_nodes_cores_count):
  159. super(GetDpdkCoreListAction, self).__init__()
  160. self.inspect_data = inspect_data
  161. self.numa_nodes_cores_count = numa_nodes_cores_count
  162. def run(self, context):
  163. dpdk_core_list = []
  164. numa_cpus_info = self.inspect_data.get('numa_topology',
  165. {}).get('cpus', [])
  166. # Checks whether numa topology cpus information is not available
  167. # in introspection data.
  168. if not numa_cpus_info:
  169. msg = 'Introspection data does not have numa_topology.cpus'
  170. return actions.Result(error=msg)
  171. # Checks whether CPU physical cores count for each NUMA nodes is
  172. # not available
  173. if not self.numa_nodes_cores_count:
  174. msg = ('CPU physical cores count for each NUMA nodes '
  175. 'is not available')
  176. return actions.Result(error=msg)
  177. numa_nodes_threads = {}
  178. # Creates list for all available threads in each NUMA node
  179. for cpu in numa_cpus_info:
  180. if not cpu['numa_node'] in numa_nodes_threads:
  181. numa_nodes_threads[cpu['numa_node']] = []
  182. numa_nodes_threads[cpu['numa_node']].extend(cpu['thread_siblings'])
  183. for node, node_cores_count in enumerate(self.numa_nodes_cores_count):
  184. # Gets least thread in NUMA node
  185. numa_node_min = min(numa_nodes_threads[node])
  186. cores_count = node_cores_count
  187. for cpu in numa_cpus_info:
  188. if cpu['numa_node'] == node:
  189. # Adds threads from core which is not having least thread
  190. if numa_node_min not in cpu['thread_siblings']:
  191. dpdk_core_list.extend(cpu['thread_siblings'])
  192. cores_count -= 1
  193. if cores_count == 0:
  194. break
  195. return ','.join([str(thread) for thread in dpdk_core_list])
  196. class GetHostCpusListAction(base.TripleOAction):
  197. """Gets the Host CPUs List.
  198. CPU threads from first physical core is allocated for host processes
  199. on each NUMA nodes.
  200. :param inspect_data: introspection data
  201. :return: Host CPUs List
  202. """
  203. def __init__(self, inspect_data):
  204. super(GetHostCpusListAction, self).__init__()
  205. self.inspect_data = inspect_data
  206. def run(self, context):
  207. host_cpus_list = []
  208. numa_cpus_info = self.inspect_data.get('numa_topology',
  209. {}).get('cpus', [])
  210. # Checks whether numa topology cpus information is not available
  211. # in introspection data.
  212. if not numa_cpus_info:
  213. msg = 'Introspection data does not have numa_topology.cpus'
  214. return actions.Result(error=msg)
  215. numa_nodes_threads = {}
  216. # Creates a list for all available threads in each NUMA nodes
  217. for cpu in numa_cpus_info:
  218. if not cpu['numa_node'] in numa_nodes_threads:
  219. numa_nodes_threads[cpu['numa_node']] = []
  220. numa_nodes_threads[cpu['numa_node']].extend(
  221. cpu['thread_siblings'])
  222. for numa_node in sorted(numa_nodes_threads.keys()):
  223. node = int(numa_node)
  224. # Gets least thread in NUMA node
  225. numa_node_min = min(numa_nodes_threads[numa_node])
  226. for cpu in numa_cpus_info:
  227. if cpu['numa_node'] == node:
  228. # Adds threads from core which is having least thread
  229. if numa_node_min in cpu['thread_siblings']:
  230. host_cpus_list.extend(cpu['thread_siblings'])
  231. break
  232. return ','.join([str(thread) for thread in host_cpus_list])
  233. class GetDpdkSocketMemoryAction(base.TripleOAction):
  234. """Gets the DPDK Socket Memory List.
  235. For NUMA node with DPDK nic, socket memory is calculated
  236. based on MTU, Overhead and Packet size in buffer.
  237. For NUMA node without DPDK nic, minimum socket memory is
  238. assigned (recommended 1GB)
  239. :param dpdk_nics_numa_info: DPDK nics numa info
  240. :param numa_nodes: list of numa nodes
  241. :param overhead: overhead value
  242. :param packet_size_in_buffer: packet size in buffer
  243. :param minimum_socket_memory: minimum socket memory
  244. :return: DPDK Socket Memory List
  245. """
  246. def __init__(self, dpdk_nics_numa_info, numa_nodes,
  247. overhead, packet_size_in_buffer,
  248. minimum_socket_memory=1024):
  249. super(GetDpdkSocketMemoryAction, self).__init__()
  250. self.dpdk_nics_numa_info = dpdk_nics_numa_info
  251. self.numa_nodes = numa_nodes
  252. self.overhead = overhead
  253. self.packet_size_in_buffer = packet_size_in_buffer
  254. self.minimum_socket_memory = minimum_socket_memory
  255. # Computes round off MTU value in bytes
  256. # example: MTU value 9000 into 9216 bytes
  257. def roundup_mtu_bytes(self, mtu):
  258. max_div_val = int(math.ceil(float(mtu) / float(1024)))
  259. return (max_div_val * 1024)
  260. # Calculates socket memory for a NUMA node
  261. def calculate_node_socket_memory(
  262. self, numa_node, dpdk_nics_numa_info, overhead,
  263. packet_size_in_buffer, minimum_socket_memory):
  264. distinct_mtu_per_node = []
  265. socket_memory = 0
  266. # For DPDK numa node
  267. for nics_info in dpdk_nics_numa_info:
  268. if (numa_node == nics_info['numa_node'] and
  269. not nics_info['mtu'] in distinct_mtu_per_node):
  270. distinct_mtu_per_node.append(nics_info['mtu'])
  271. roundup_mtu = self.roundup_mtu_bytes(nics_info['mtu'])
  272. socket_memory += (((roundup_mtu + overhead)
  273. * packet_size_in_buffer) /
  274. (1024 * 1024))
  275. # For Non DPDK numa node
  276. if socket_memory == 0:
  277. socket_memory = minimum_socket_memory
  278. # For DPDK numa node
  279. else:
  280. socket_memory += 512
  281. socket_memory_in_gb = int(socket_memory / 1024)
  282. if socket_memory % 1024 > 0:
  283. socket_memory_in_gb += 1
  284. return (socket_memory_in_gb * 1024)
  285. def run(self, context):
  286. dpdk_socket_memory_list = []
  287. for node in self.numa_nodes:
  288. socket_mem = self.calculate_node_socket_memory(
  289. node, self.dpdk_nics_numa_info, self.overhead,
  290. self.packet_size_in_buffer,
  291. self.minimum_socket_memory)
  292. dpdk_socket_memory_list.append(socket_mem)
  293. return ','.join([str(sm) for sm in dpdk_socket_memory_list])
  294. class ConvertNumberToRangeListAction(base.TripleOAction):
  295. """Converts number list into range list
  296. :param num_list: comma delimited number list as string
  297. :return: comma delimited range list as string
  298. """
  299. def __init__(self, num_list):
  300. super(ConvertNumberToRangeListAction, self).__init__()
  301. self.num_list = num_list
  302. # converts number list into range list.
  303. # here input parameter and return value as list
  304. # example: [12, 13, 14, 17] into ["12-14", "17"]
  305. def convert_number_to_range_list(self, num_list):
  306. num_list.sort()
  307. range_list = []
  308. range_min = num_list[0]
  309. for num in num_list:
  310. next_val = num + 1
  311. if next_val not in num_list:
  312. if range_min != num:
  313. range_list.append(str(range_min) + '-' + str(num))
  314. else:
  315. range_list.append(str(range_min))
  316. next_index = num_list.index(num) + 1
  317. if next_index < len(num_list):
  318. range_min = num_list[next_index]
  319. # here, range_list is a list of strings
  320. return range_list
  321. def run(self, context):
  322. try:
  323. if not self.num_list:
  324. err_msg = ("Input param 'num_list' is blank.")
  325. raise exception.DeriveParamsError(err_msg)
  326. try:
  327. # splitting a string (comma delimited list) into
  328. # list of numbers
  329. # example: "12,13,14,17" string into [12,13,14,17]
  330. num_list = [int(num.strip(' '))
  331. for num in self.num_list.split(",")]
  332. except ValueError as exc:
  333. err_msg = ("Invalid number in input param "
  334. "'num_list': %s" % exc)
  335. raise exception.DeriveParamsError(err_msg)
  336. range_list = self.convert_number_to_range_list(num_list)
  337. except exception.DeriveParamsError as err:
  338. LOG.error('Derive Params Error: %s', err)
  339. return actions.Result(error=str(err))
  340. # converts into comma delimited range list as string
  341. return ','.join(range_list)
  342. class ConvertRangeToNumberListAction(base.TripleOAction):
  343. """Converts range list to integer list
  344. :param range_list: comma delimited range list as string / list
  345. :return: comma delimited number list as string
  346. """
  347. def __init__(self, range_list):
  348. super(ConvertRangeToNumberListAction, self).__init__()
  349. self.range_list = range_list
  350. # converts range list into number list
  351. # here input parameter and return value as list
  352. # example: ["12-14", "^13", "17"] into [12, 14, 17]
  353. def convert_range_to_number_list(self, range_list):
  354. num_list = []
  355. exclude_num_list = []
  356. try:
  357. for val in range_list:
  358. val = val.strip(' ')
  359. if '^' in val:
  360. exclude_num_list.append(int(val[1:]))
  361. elif '-' in val:
  362. split_list = val.split("-")
  363. range_min = int(split_list[0])
  364. range_max = int(split_list[1])
  365. num_list.extend(range(range_min, (range_max + 1)))
  366. else:
  367. num_list.append(int(val))
  368. except ValueError as exc:
  369. err_msg = ("Invalid number in input param "
  370. "'range_list': %s" % exc)
  371. raise exception.DeriveParamsError(err_msg)
  372. # here, num_list is a list of integers
  373. return [num for num in num_list if num not in exclude_num_list]
  374. def run(self, context):
  375. try:
  376. if not self.range_list:
  377. err_msg = ("Input param 'range_list' is blank.")
  378. raise exception.DeriveParamsError(err_msg)
  379. range_list = self.range_list
  380. # converts into python list if range_list is not list type
  381. if not isinstance(range_list, list):
  382. range_list = self.range_list.split(",")
  383. num_list = self.convert_range_to_number_list(range_list)
  384. except exception.DeriveParamsError as err:
  385. LOG.error('Derive Params Error: %s', err)
  386. return actions.Result(error=str(err))
  387. # converts into comma delimited number list as string
  388. return ','.join([str(num) for num in num_list])