A Python agent for provisioning and deprovisioning Bare Metal servers.
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.
 
 
 

256 lines
9.7 KiB

  1. # Copyright 2017 Red Hat, Inc.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
  12. # implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. import os
  16. from oslo_log import log
  17. import pint
  18. from ironic_python_agent import errors
  19. LOG = log.getLogger(__name__)
  20. UNIT_CONVERTER = pint.UnitRegistry(filename=None)
  21. UNIT_CONVERTER.define('kB = []')
  22. UNIT_CONVERTER.define('KB = []')
  23. UNIT_CONVERTER.define('MB = 1024 KB')
  24. UNIT_CONVERTER.define('GB = 1048576 KB')
  25. def get_numa_node_id(numa_node_dir):
  26. """Provides the NUMA node id from NUMA node directory
  27. :param numa_node_dir: NUMA node directory
  28. :raises: IncompatibleNumaFormatError: when unexpected format data
  29. in NUMA node dir
  30. :return: NUMA node id
  31. """
  32. try:
  33. return int(os.path.basename(numa_node_dir)[4:])
  34. except (IOError, ValueError, IndexError) as exc:
  35. msg = ('Failed to get NUMA node id for %(node)s: '
  36. '%(error)s' % {'node': numa_node_dir, 'error': exc})
  37. raise errors.IncompatibleNumaFormatError(msg)
  38. def get_nodes_memory_info(numa_node_dirs):
  39. """Collect the NUMA nodes memory information.
  40. "ram": [{"numa_node": <numa_node_id>, "size_kb": <memory_in_kb>}, ...]
  41. :param numa_node_dirs: A list of NUMA node directories
  42. :raises: IncompatibleNumaFormatError: when unexpected format data
  43. in NUMA node
  44. :return: A list of memory information with NUMA node id
  45. """
  46. ram = []
  47. for numa_node_dir in numa_node_dirs:
  48. numa_node_memory = {}
  49. numa_node_id = get_numa_node_id(numa_node_dir)
  50. try:
  51. with open(os.path.join(numa_node_dir,
  52. 'meminfo')) as meminfo_file:
  53. for line in meminfo_file:
  54. if 'MemTotal' in line:
  55. break
  56. else:
  57. msg = ('Memory information is not available for '
  58. '%(node)s' % {'node': numa_node_dir})
  59. raise errors.IncompatibleNumaFormatError(msg)
  60. except IOError as exc:
  61. msg = ('Failed to get memory information '
  62. 'for %(node)s: %(error)s' %
  63. {'node': numa_node_dir, 'error': exc})
  64. raise errors.IncompatibleNumaFormatError(msg)
  65. try:
  66. # To get memory size with unit from memory info line
  67. # Memory info sample line format 'Node 0 MemTotal: 1560000 kB'
  68. value = line.split(":")[1].strip()
  69. memory_kb = int(UNIT_CONVERTER(value).to_base_units().magnitude)
  70. except (ValueError, IndexError, pint.UndefinedUnitError) as exc:
  71. msg = ('Failed to get memory information for %(node)s: '
  72. '%(error)s' % {'node': numa_node_dir, 'error': exc})
  73. raise errors.IncompatibleNumaFormatError(msg)
  74. numa_node_memory['numa_node'] = numa_node_id
  75. numa_node_memory['size_kb'] = memory_kb
  76. LOG.debug('Found memory available %d KB in NUMA node %d',
  77. memory_kb, numa_node_id)
  78. ram.append(numa_node_memory)
  79. return ram
  80. def get_nodes_cores_info(numa_node_dirs):
  81. """Collect the NUMA nodes cpu's and thread's information.
  82. "cpus": [
  83. {
  84. "cpu": <cpu_id>, "numa_node": <numa_node_id>,
  85. "thread_siblings": [<list of sibling threads>]
  86. },
  87. ...,
  88. ]
  89. NUMA nodes path: /sys/devices/system/node/node<node_id>
  90. Thread dirs path: /sys/devices/system/node/node<node_id>/cpu<thread_id>
  91. CPU id file path: /sys/devices/system/node/node<node_id>/cpu<thread_id>/
  92. topology/core_id
  93. :param numa_node_dirs: A list of NUMA node directories
  94. :raises: IncompatibleNumaFormatError: when unexpected format data
  95. in NUMA node
  96. :return: A list of cpu information with NUMA node id and thread siblings
  97. """
  98. dict_cpus = {}
  99. for numa_node_dir in numa_node_dirs:
  100. numa_node_id = get_numa_node_id(numa_node_dir)
  101. try:
  102. thread_dirs = os.listdir(numa_node_dir)
  103. except OSError as exc:
  104. msg = ('Failed to get list of threads for %(node)s: '
  105. '%(error)s' % {'node': numa_node_dir, 'error': exc})
  106. raise errors.IncompatibleNumaFormatError(msg)
  107. for thread_dir in thread_dirs:
  108. if (not os.path.isdir(os.path.join(numa_node_dir, thread_dir))
  109. or not thread_dir.startswith("cpu")):
  110. continue
  111. try:
  112. thread_id = int(thread_dir[3:])
  113. except (ValueError, IndexError) as exc:
  114. msg = ('Failed to get cores information for '
  115. '%(node)s: %(error)s' %
  116. {'node': numa_node_dir, 'error': exc})
  117. raise errors.IncompatibleNumaFormatError(msg)
  118. try:
  119. with open(os.path.join(numa_node_dir, thread_dir, 'topology',
  120. 'core_id')) as core_id_file:
  121. cpu_id = int(core_id_file.read().strip())
  122. except (IOError, ValueError) as exc:
  123. msg = ('Failed to gather cpu_id for thread'
  124. '%(thread)s NUMA node %(node)s: %(error)s' %
  125. {'thread': thread_dir, 'node': numa_node_dir,
  126. 'error': exc})
  127. raise errors.IncompatibleNumaFormatError(msg)
  128. # CPU and NUMA node together forms a unique value, as cpu_id is
  129. # specific to a NUMA node
  130. # NUMA node id and cpu id tuple is used for unique key
  131. dict_key = numa_node_id, cpu_id
  132. if dict_key in dict_cpus:
  133. if thread_id not in dict_cpus[dict_key]['thread_siblings']:
  134. dict_cpus[dict_key]['thread_siblings'].append(thread_id)
  135. else:
  136. cpu_item = {}
  137. cpu_item['thread_siblings'] = [thread_id]
  138. cpu_item['cpu'] = cpu_id
  139. cpu_item['numa_node'] = numa_node_id
  140. dict_cpus[dict_key] = cpu_item
  141. LOG.debug('Found a thread sibling %d for CPU %d in NUMA node %d',
  142. thread_id, cpu_id, numa_node_id)
  143. return list(dict_cpus.values())
  144. def get_nodes_nics_info(nic_device_path):
  145. """Collect the NUMA nodes nics information.
  146. "nics": [
  147. {"name": "<network interface name>", "numa_node": <numa_node_id>},
  148. ...,
  149. ]
  150. :param nic_device_path: nic device directory path
  151. :raises: IncompatibleNumaFormatError: when unexpected format data
  152. in NUMA node
  153. :return: A list of nics information with NUMA node id
  154. """
  155. nics = []
  156. if not os.path.isdir(nic_device_path):
  157. msg = ('Failed to get list of NIC\'s, NIC device path '
  158. 'does not exist: %(nic_device_path)s' %
  159. {'nic_device_path': nic_device_path})
  160. raise errors.IncompatibleNumaFormatError(msg)
  161. for nic_dir in os.listdir(nic_device_path):
  162. if not os.path.isdir(os.path.join(nic_device_path, nic_dir, 'device')):
  163. continue
  164. try:
  165. with open(os.path.join(nic_device_path, nic_dir, 'device',
  166. 'numa_node')) as nicsinfo_file:
  167. numa_node_id = int(nicsinfo_file.read().strip())
  168. except (IOError, ValueError) as exc:
  169. msg = ('Failed to gather NIC\'s for NUMA node %(node)s: '
  170. '%(error)s' % {'node': nic_dir, 'error': exc})
  171. raise errors.IncompatibleNumaFormatError(msg)
  172. numa_node_nics = {}
  173. numa_node_nics['name'] = nic_dir
  174. numa_node_nics['numa_node'] = numa_node_id
  175. LOG.debug('Found a NIC %s in NUMA node %d', nic_dir,
  176. numa_node_id)
  177. nics.append(numa_node_nics)
  178. return nics
  179. def collect_numa_topology_info(data, failures):
  180. """Collect the NUMA topology information.
  181. {
  182. "numa_topology": {
  183. "ram": [{"numa_node": <numa_node_id>, "size_kb": <memory_in_kb>}, ...],
  184. "cpus": [
  185. {
  186. "cpu": <cpu_id>, "numa_node": <numa_node_id>,
  187. "thread_siblings": [<list of sibling threads>]
  188. },
  189. ...,
  190. ],
  191. "nics": [
  192. {"name": "<network interface name>", "numa_node": <numa_node_id>},
  193. ...,
  194. ]
  195. }
  196. }
  197. The data is gathered from /sys/devices/system/node/node<X> and
  198. /sys/class/net/ directories.
  199. :param data: mutable data that we'll send to inspector
  200. :param failures: AccumulatedFailures object
  201. :return: None
  202. """
  203. numa_node_path = '/sys/devices/system/node/'
  204. nic_device_path = '/sys/class/net/'
  205. numa_info = {}
  206. numa_node_dirs = []
  207. if not os.path.isdir(numa_node_path):
  208. LOG.warning('Failed to get list of NUMA nodes, NUMA node path '
  209. 'does not exist: %s', numa_node_path)
  210. return
  211. for numa_node_dir in os.listdir(numa_node_path):
  212. numa_node_dir_path = os.path.join(numa_node_path, numa_node_dir)
  213. if (os.path.isdir(numa_node_dir_path)
  214. and numa_node_dir.startswith("node")):
  215. numa_node_dirs.append(numa_node_dir_path)
  216. try:
  217. numa_info['ram'] = get_nodes_memory_info(numa_node_dirs)
  218. numa_info['cpus'] = get_nodes_cores_info(numa_node_dirs)
  219. numa_info['nics'] = get_nodes_nics_info(nic_device_path)
  220. except errors.IncompatibleNumaFormatError as exc:
  221. LOG.warning('Failed to get some NUMA information (%s)', exc)
  222. return
  223. data['numa_topology'] = numa_info