config/sysinv/sysinv/sysinv/sysinv/agent/node.py

574 lines
21 KiB
Python

#
# Copyright (c) 2013-2016 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# All Rights Reserved.
#
""" inventory numa node Utilities and helper functions."""
import os
from os import listdir
from os.path import isfile
from os.path import join
import re
from oslo_log import log as logging
import tsconfig.tsconfig as tsc
LOG = logging.getLogger(__name__)
# Defines the size of one kilobyte
SIZE_KB = 1024
# Defines the size of 2 megabytes in kilobyte units
SIZE_2M_KB = 2048
# Defines the size of 1 gigabyte in kilobyte units
SIZE_1G_KB = 1048576
# Defines the size of 2 megabytes in megabyte units
SIZE_2M_MB = int(SIZE_2M_KB // SIZE_KB)
# Defines the size of 1 gigabyte in megabyte units
SIZE_1G_MB = int(SIZE_1G_KB // SIZE_KB)
# Defines the minimum size of memory for a controller node in megabyte units
CONTROLLER_MIN_MB = 6000
# Defines the minimum size of memory for a worker node in megabyte units
COMPUTE_MIN_MB = 1600
# Defines the minimum size of memory for a secondary worker node in megabyte
# units
COMPUTE_MIN_NON_0_MB = 500
class CPU(object):
'''Class to encapsulate CPU data for System Inventory'''
def __init__(self, cpu, numa_node, core, thread,
cpu_family=None, cpu_model=None, revision=None):
'''Construct a Icpu object with the given values.'''
self.cpu = cpu
self.numa_node = numa_node
self.core = core
self.thread = thread
self.cpu_family = cpu_family
self.cpu_model = cpu_model
self.revision = revision
# self.allocated_functions = mgmt (usu. 0), vswitch
def __eq__(self, rhs):
return (self.cpu == rhs.cpu and
self.numa_node == rhs.numa_node and
self.core == rhs.core and
self.thread == rhs.thread)
def __hash__(self):
return hash((self.cpu, self.numa_node, self.core, self.thread,
self.cpu_family, self.cpu_model, self.revision))
def __ne__(self, rhs):
return (self.cpu != rhs.cpu or
self.numa_node != rhs.numa_node or
self.core != rhs.core or
self.thread != rhs.thread)
def __str__(self):
return "%s [%s] [%s] [%s]" % (self.cpu, self.numa_node,
self.core, self.thread)
def __repr__(self):
return "<CPU '%s'>" % str(self)
class NodeOperator(object):
'''Class to encapsulate CPU operations for System Inventory'''
def __init__(self):
self.num_cpus = 0
self.num_nodes = 0
self.float_cpuset = 0
self.total_memory_mb = 0
self.free_memory_mb = 0
self.total_memory_nodes_mb = []
self.free_memory_nodes_mb = []
self.topology = {}
# self._get_cpu_topology()
# self._get_total_memory_mb()
# self._get_total_memory_nodes_mb()
# self._get_free_memory_mb()
# self._get_free_memory_nodes_mb()
def convert_range_string_to_list(self, s):
olist = []
s = s.strip()
if s:
for part in s.split(','):
if '-' in part:
a, b = part.split('-')
a, b = int(a), int(b)
olist.extend(range(a, b + 1))
else:
a = int(part)
olist.append(a)
olist.sort()
return olist
def inodes_get_inumas_icpus(self):
'''Enumerate logical cpu topology based on parsing /proc/cpuinfo
as function of socket_id, core_id, and thread_id. This updates
topology.
:param self
:updates self.num_cpus- number of logical cpus
:updates self.num_nodes- number of sockets;maps to number of numa nodes
:updates self.topology[socket_id][core_id][thread_id] = cpu
:returns None
'''
self.num_cpus = 0
self.num_nodes = 0
self.topology = {}
thread_cnt = {}
cpu = socket_id = core_id = thread_id = -1
re_processor = re.compile(r'^[Pp]rocessor\s+:\s+(\d+)')
re_socket = re.compile(r'^physical id\s+:\s+(\d+)')
re_core = re.compile(r'^core id\s+:\s+(\d+)')
re_cpu_family = re.compile(r'^cpu family\s+:\s+(\d+)')
re_cpu_model = re.compile(r'^model name\s+:\s+(\w+)')
inumas = []
icpus = []
sockets = []
with open('/proc/cpuinfo', 'r') as infile:
icpu_attrs = {}
for line in infile:
match = re_processor.search(line)
if match:
cpu = int(match.group(1))
socket_id = -1
core_id = -1
thread_id = -1
self.num_cpus += 1
continue
match = re_cpu_family.search(line)
if match:
name_value = [s.strip() for s in line.split(':', 1)]
name, value = name_value
icpu_attrs.update({'cpu_family': value})
continue
match = re_cpu_model.search(line)
if match:
name_value = [s.strip() for s in line.split(':', 1)]
name, value = name_value
icpu_attrs.update({'cpu_model': value})
continue
match = re_socket.search(line)
if match:
socket_id = int(match.group(1))
if socket_id not in sockets:
sockets.append(socket_id)
attrs = {
'numa_node': socket_id,
'capabilities': {},
}
inumas.append(attrs)
continue
match = re_core.search(line)
if match:
core_id = int(match.group(1))
if socket_id not in thread_cnt:
thread_cnt[socket_id] = {}
if core_id not in thread_cnt[socket_id]:
thread_cnt[socket_id][core_id] = 0
else:
thread_cnt[socket_id][core_id] += 1
thread_id = thread_cnt[socket_id][core_id]
if socket_id not in self.topology:
self.topology[socket_id] = {}
if core_id not in self.topology[socket_id]:
self.topology[socket_id][core_id] = {}
self.topology[socket_id][core_id][thread_id] = cpu
attrs = {
'cpu': cpu,
'numa_node': socket_id,
'core': core_id,
'thread': thread_id,
'capabilities': {},
}
icpu_attrs.update(attrs)
icpus.append(icpu_attrs)
icpu_attrs = {}
continue
self.num_nodes = len(list(self.topology.keys()))
# In the case topology not detected, hard-code structures
if self.num_nodes == 0:
n_sockets, n_cores, n_threads = (1, int(self.num_cpus), 1)
self.topology = {}
for socket_id in range(n_sockets):
self.topology[socket_id] = {}
if socket_id not in sockets:
sockets.append(socket_id)
attrs = {
'numa_node': socket_id,
'capabilities': {},
}
inumas.append(attrs)
for core_id in range(n_cores):
self.topology[socket_id][core_id] = {}
for thread_id in range(n_threads):
self.topology[socket_id][core_id][thread_id] = 0
attrs = {
'cpu': cpu,
'numa_node': socket_id,
'core': core_id,
'thread': thread_id,
'capabilities': {},
}
icpus.append(attrs)
# Define Thread-Socket-Core order for logical cpu enumeration
cpu = 0
for thread_id in range(n_threads):
for core_id in range(n_cores):
for socket_id in range(n_sockets):
if socket_id not in sockets:
sockets.append(socket_id)
attrs = {
'numa_node': socket_id,
'capabilities': {},
}
inumas.append(attrs)
self.topology[socket_id][core_id][thread_id] = cpu
attrs = {
'cpu': cpu,
'numa_node': socket_id,
'core': core_id,
'thread': thread_id,
'capabilities': {},
}
icpus.append(attrs)
cpu += 1
self.num_nodes = len(list(self.topology.keys()))
LOG.debug("inumas= %s, icpus = %s" % (inumas, icpus))
return inumas, icpus
def _get_immediate_subdirs(self, dir):
return [name for name in listdir(dir)
if os.path.isdir(join(dir, name))]
def _get_vswitch_reserved_memory(self, node):
# Read vswitch memory from worker_reserved.conf
vswitch_hugepages_nr = 0
vswitch_hugepages_size = 0
try:
with open('/etc/platform/worker_reserved.conf', 'r') as infile:
for line in infile:
if line.startswith("COMPUTE_VSWITCH_MEMORY="):
val = line.split("=")
vswitch_reserves = val[1].strip('\n')[1:-1]
for idx, reserve in enumerate(vswitch_reserves.split()):
if idx != node:
continue
reserve = reserve.split(":")
if reserve[0].strip('"') == "node%d" % node:
pages_nr = re.sub('[^0-9]', '', reserve[2])
pages_size = reserve[1]
vswitch_hugepages_nr = int(pages_nr)
if pages_size == "1048576kB":
vswitch_hugepages_size = SIZE_1G_MB
else:
vswitch_hugepages_size = SIZE_2M_MB
break
except Exception as e:
LOG.debug("Could not read vswitch reserved memory: %s", e)
return vswitch_hugepages_nr, vswitch_hugepages_size
def _inode_get_memory_hugepages(self):
"""Collect hugepage info, including vswitch, and vm.
Collect platform reserved if config.
:param self
:returns list of memory nodes and attributes
"""
imemory = []
initial_worker_config_completed = \
os.path.exists(tsc.INITIAL_WORKER_CONFIG_COMPLETE)
# check if it is initial report before the huge pages are allocated
initial_report = not initial_worker_config_completed
# do not send report if the initial worker config is completed and
# worker config has not finished, i.e.during subsequent
# reboot before the manifest allocates the huge pages
worker_config_completed = \
os.path.exists(tsc.VOLATILE_WORKER_CONFIG_COMPLETE)
if (initial_worker_config_completed and
not worker_config_completed):
return imemory
for node in range(self.num_nodes):
attr = {}
total_hp_mb = 0 # Total memory (MB) currently configured in HPs
free_hp_mb = 0
# Check vswitch and libvirt memory
# Loop through configured hugepage sizes of this node and record
# total number and number free
hugepages = "/sys/devices/system/node/node%d/hugepages" % node
try:
subdirs = self._get_immediate_subdirs(hugepages)
for subdir in subdirs:
hp_attr = {}
sizesplit = subdir.split('-')
if sizesplit[1].startswith("1048576kB"):
size = SIZE_1G_MB
else:
size = SIZE_2M_MB
nr_hugepages = 0
free_hugepages = 0
mydir = hugepages + '/' + subdir
files = [f for f in listdir(mydir) if isfile(join(mydir, f))]
if files:
for file in files:
if file.startswith("nr_hugepages"):
with open(mydir + '/' + file, 'r') as f:
nr_hugepages = int(f.readline())
if file.startswith("free_hugepages"):
with open(mydir + '/' + file, 'r') as f:
free_hugepages = int(f.readline())
total_hp_mb = total_hp_mb + int(nr_hugepages * size)
free_hp_mb = free_hp_mb + int(free_hugepages * size)
vs_hp_nr, vs_hp_size = self._get_vswitch_reserved_memory(
node)
if vs_hp_size == 0:
vs_hp_size = size
# Libvirt hugepages can be 1G and 2M
if size == SIZE_1G_MB:
hp_attr = {}
if vs_hp_size == size:
# If the huge pages are not allocated
if nr_hugepages != 0:
nr_hugepages -= vs_hp_nr
hp_attr.update({
'vswitch_hugepages_size_mib': vs_hp_size,
'vswitch_hugepages_nr': vs_hp_nr,
'vswitch_hugepages_avail': 0
})
hp_attr.update({
'vm_hugepages_nr_1G': nr_hugepages,
'vm_hugepages_avail_1G': free_hugepages,
'vm_hugepages_use_1G': True
})
else:
if len(subdirs) == 1:
# No 1G hugepage support.
hp_attr = {
'vm_hugepages_use_1G': False,
'vm_hugepages_nr_1G': 0,
'vswitch_hugepages_size_mib': vs_hp_size,
'vswitch_hugepages_nr': vs_hp_nr,
'vswitch_hugepages_avail': 0
}
if nr_hugepages != 0:
nr_hugepages -= vs_hp_nr
else:
hp_attr = {}
if vs_hp_size == size and initial_report is False:
# User manually set 2M pages
if nr_hugepages != 0:
nr_hugepages -= vs_hp_nr
hp_attr.update({
'vswitch_hugepages_size_mib': vs_hp_size,
'vswitch_hugepages_nr': vs_hp_nr,
'vswitch_hugepages_avail': 0
})
hp_attr.update({
'vm_hugepages_avail_2M': free_hugepages,
'vm_hugepages_nr_2M': nr_hugepages
})
attr.update(hp_attr)
except IOError:
# silently ignore IO errors (eg. file missing)
pass
# Get the total memory from meminfo for this node
re_node_memtotal = re.compile(r'^Node\s+\d+\s+MemTotal:\s+(\d+)')
total_kb = 0 # Total Memory (KB)
meminfo = "/sys/devices/system/node/node%d/meminfo" % node
try:
with open(meminfo, 'r') as infile:
for line in infile:
match = re_node_memtotal.search(line)
if match:
total_kb += int(match.group(1))
break
except IOError:
# silently ignore IO errors (eg. file missing)
pass
node_total_kb = total_kb
# Read base memory from worker_reserved.conf
base_mem_mb = 0
with open('/etc/platform/worker_reserved.conf', 'r') as infile:
for line in infile:
if "WORKER_BASE_RESERVED" in line:
val = line.split("=")
base_reserves = val[1].strip('\n')[1:-1]
for reserve in base_reserves.split():
reserve = reserve.split(":")
if reserve[0].strip('"') == "node%d" % node:
base_mem_mb = int(reserve[1].strip('MB'))
# On small systems, clip memory overhead to more reasonable minimal
# settings
if (total_kb // SIZE_KB - base_mem_mb) < 1000:
if node == 0:
base_mem_mb = COMPUTE_MIN_MB
if tsc.nodetype == 'controller':
base_mem_mb += CONTROLLER_MIN_MB
else:
base_mem_mb = COMPUTE_MIN_NON_0_MB
eng_kb = node_total_kb - base_mem_mb * SIZE_KB
vswitch_mem_kb = (attr.get('vswitch_hugepages_size_mib', 0) *
attr.get('vswitch_hugepages_nr', 0) * SIZE_KB)
vm_kb = (eng_kb - vswitch_mem_kb)
max_vm_pages_2mb = vm_kb // SIZE_2M_KB
max_vm_pages_1gb = vm_kb // SIZE_1G_KB
attr.update({
'vm_hugepages_possible_2M': max_vm_pages_2mb,
'vm_hugepages_possible_1G': max_vm_pages_1gb,
})
attr.update({
'numa_node': node,
'memtotal_mib': total_hp_mb,
'memavail_mib': free_hp_mb,
'hugepages_configured': True,
'node_memtotal_mib': node_total_kb // SIZE_KB,
})
imemory.append(attr)
return imemory
def _inode_get_memory_nonhugepages(self):
'''Collect nonhugepage info, including platform reserved if config.
:param self
:returns list of memory nodes and attributes
'''
imemory = []
self.total_memory_mb = 0
re_node_memtotal = re.compile(r'^Node\s+\d+\s+MemTotal:\s+(\d+)')
re_node_memfree = re.compile(r'^Node\s+\d+\s+MemFree:\s+(\d+)')
re_node_filepages = re.compile(r'^Node\s+\d+\s+FilePages:\s+(\d+)')
re_node_sreclaim = re.compile(r'^Node\s+\d+\s+SReclaimable:\s+(\d+)')
for node in range(self.num_nodes):
attr = {}
total_mb = 0
free_mb = 0
meminfo = "/sys/devices/system/node/node%d/meminfo" % node
try:
with open(meminfo, 'r') as infile:
for line in infile:
match = re_node_memtotal.search(line)
if match:
total_mb += int(match.group(1))
continue
match = re_node_memfree.search(line)
if match:
free_mb += int(match.group(1))
continue
match = re_node_filepages.search(line)
if match:
free_mb += int(match.group(1))
continue
match = re_node_sreclaim.search(line)
if match:
free_mb += int(match.group(1))
continue
except IOError:
# silently ignore IO errors (eg. file missing)
pass
total_mb /= 1024
free_mb /= 1024
self.total_memory_nodes_mb.append(total_mb)
attr = {
'numa_node': node,
'memtotal_mib': total_mb,
'memavail_mib': free_mb,
'hugepages_configured': False,
}
imemory.append(attr)
return imemory
def inodes_get_imemory(self):
'''Collect logical memory topology
:param self
:returns list of memory nodes and attributes
'''
imemory = []
if os.path.isfile("/etc/platform/worker_reserved.conf"):
imemory = self._inode_get_memory_hugepages()
else:
imemory = self._inode_get_memory_nonhugepages()
LOG.debug("imemory= %s" % imemory)
return imemory