Merge "add sysinv support for specifying cpu function by range"

This commit is contained in:
Zuul 2021-04-01 14:29:26 +00:00 committed by Gerrit Code Review
commit 7ce5367d57
6 changed files with 203 additions and 30 deletions

View File

@ -90,6 +90,9 @@ def do_host_cpu_list(cc, args):
choices=['vswitch', 'shared', 'platform', 'application-isolated'],
required=True,
help='The Core Function.')
@utils.arg('-c', '--cpulist',
metavar='<cpulist>',
help="List of cpus, mutually exclusive with the -pX options")
@utils.arg('-p0', '--num_cores_on_processor0',
metavar='<num_cores_on_processor0>',
type=int,
@ -108,7 +111,7 @@ def do_host_cpu_list(cc, args):
help='Number of cores on Processor 3.')
def do_host_cpu_modify(cc, args):
"""Modify cpu core assignments."""
field_list = ['function', 'allocated_function',
field_list = ['function', 'allocated_function', 'cpulist',
'num_cores_on_processor0', 'num_cores_on_processor1',
'num_cores_on_processor2', 'num_cores_on_processor3']
@ -119,18 +122,25 @@ def do_host_cpu_modify(cc, args):
if k in field_list and not (v is None))
cap = {'function': user_specified_fields.get('function')}
cpulist = user_specified_fields.get('cpulist')
if cpulist:
cap['cpulist'] = cpulist
for k, v in user_specified_fields.items():
if k.startswith('num_cores_on_processor'):
sockets.append({k.lstrip('num_cores_on_processor'): v})
# can't specify both the -c option and any of the -pX options
if sockets and cpulist:
raise exc.CommandError('Not allowed to specify both -c and -pX options.')
if sockets:
cap.update({'sockets': sockets})
capabilities.append(cap)
else:
elif not cpulist:
raise exc.CommandError('Number of cores on Processor (Socket) '
'not provided.')
capabilities.append(cap)
icpus = cc.ihost.host_cpus_modify(ihost.uuid, capabilities)
field_labels = ['uuid', 'log_core', 'processor', 'phy_core', 'thread',

View File

@ -84,6 +84,9 @@ class CPU(base.APIBase):
function = wtypes.text
"Represent the function of the icpu"
cpulist = wtypes.text
"The list of CPUs for this function"
num_cores_on_processor0 = wtypes.text
"The number of cores on processors 0"
@ -126,6 +129,8 @@ class CPU(base.APIBase):
# API only attributes
self.fields.append('function')
setattr(self, 'function', kwargs.get('function', None))
self.fields.append('cpulist')
setattr(self, 'cpulist', kwargs.get('cpulist', None))
self.fields.append('num_cores_on_processor0')
setattr(self, 'num_cores_on_processor0',
kwargs.get('num_cores_on_processor0', None))

View File

@ -208,6 +208,29 @@ def get_cpu_counts(host):
return counts
def append_ht_sibling(host, cpu_list):
"""Append to cpu_list the hyperthread siblings for the cpus in the list"""
# TODO: Add UTs for this.
# There's probably a more efficient way to do this.
cpus_to_add = []
for cpu_num in cpu_list:
# Get node/core for specified cpu number
for cpu in host.cpus:
if cpu.cpu == cpu_num:
# We've found the cpu of interest, now check for siblings
for cpu2 in host.cpus:
if cpu2.numa_node == cpu.numa_node and \
cpu2.core == cpu.core and \
cpu2.thread != cpu.thread:
cpus_to_add.append(cpu2.cpu)
break
break
# Add in the HT siblings, then remove any duplicates.
cpus_to_add.extend(cpu_list)
return list(set(cpus_to_add))
def init_cpu_counts(host):
"""Create empty data structures to track CPU assignments by socket and
function."""
@ -249,8 +272,22 @@ def restructure_host_cpu_data(host):
host.cpu_lists[cpu.numa_node].append(int(cpu.cpu))
def check_core_allocations(host, cpu_counts):
def check_core_allocations(host, cpu_counts, cpu_lists=None):
"""Check that minimum and maximum core values are respected."""
if cpu_lists:
# Verify no overlaps in cpulists for different functions. Not all
# functions are guaranteed to be present as keys in cpu_lists.
cpulist = []
for function in CORE_FUNCTIONS:
functionlist = cpu_lists.get(function, [])
if set(cpulist).intersection(functionlist):
raise wsme.exc.ClientSideError(
"Some CPUs are specified for more than one function.")
cpulist.extend(functionlist)
# NOTE: contrary to the variable names, these are actually logical CPUs
# rather than cores, so if hyperthreading is enabled they're SMT siblings.
total_platform_cores = 0
total_vswitch_cores = 0
total_shared_cores = 0
@ -272,7 +309,15 @@ def check_core_allocations(host, cpu_counts):
total_shared_cores += shared_cores
total_isolated_cores += isolated_cores
# Validate Platform cores
# Add any cpus specified via ranges to the totals.
# Note: Can't specify by both count and range for the same function.
if cpu_lists:
total_platform_cores += len(cpu_lists.get(constants.PLATFORM_FUNCTION, []))
total_vswitch_cores += len(cpu_lists.get(constants.VSWITCH_FUNCTION, []))
total_shared_cores += len(cpu_lists.get(constants.SHARED_FUNCTION, []))
total_isolated_cores += len(cpu_lists.get(constants.ISOLATED_FUNCTION, []))
# Validate Platform cores (actually logical CPUs)
if ((constants.CONTROLLER in host.subfunctions) and
(constants.WORKER in host.subfunctions)):
if total_platform_cores < 2:
@ -282,7 +327,7 @@ def check_core_allocations(host, cpu_counts):
raise wsme.exc.ClientSideError("%s must have at least one core." %
constants.PLATFORM_FUNCTION)
# Validate shared cores
# Validate shared cores (actually logical CPUs)
for s in range(0, len(host.nodes)):
shared_cores = cpu_counts[s][constants.SHARED_FUNCTION]
if host.hyperthreading:
@ -292,7 +337,7 @@ def check_core_allocations(host, cpu_counts):
'%s cores are limited to 1 per processor.'
% constants.SHARED_FUNCTION)
# Validate vswitch cores
# Validate vswitch cores (actually logical CPUs)
if total_vswitch_cores != 0:
vswitch_type = cutils.get_vswitch_type(pecan.request.dbapi)
if constants.VSWITCH_TYPE_NONE == vswitch_type:
@ -308,7 +353,7 @@ def check_core_allocations(host, cpu_counts):
"The %s function can only be assigned up to %s cores." %
(constants.VSWITCH_FUNCTION.lower(), VSWITCH_MAX_CORES))
# Validate Isolated cores:
# Validate Isolated cores: (actually logical CPUs)
# - Prevent isolated core assignment if vswitch or shared cores are
# allocated.
if total_isolated_cores > 0:
@ -326,31 +371,57 @@ def check_core_allocations(host, cpu_counts):
constants.APPLICATION_FUNCTION)
def update_core_allocations(host, cpu_counts):
def node_from_cpu(host, cpu_num):
for cpu in host.cpus:
if cpu.cpu == cpu_num:
return cpu.numa_node
raise wsme.exc.ClientSideError("Specified CPU %s is invalid." % cpu_num)
def update_core_allocations(host, cpu_counts, cpulists=None):
"""Update the per socket/function cpu list based on the newly requested
counts."""
# Remove any previous assignments
for s in range(0, len(host.nodes)):
for f in CORE_FUNCTIONS:
host.cpu_functions[s][f] = []
# Set new assignments
# Make per-numa-node lists of available CPUs
cpu_lists = {}
for s in range(0, len(host.nodes)):
cpu_lists[s] = list(host.cpu_lists[s]) if s in host.cpu_lists else []
# We need to reserve all of the cpulist-specified CPUs first, then
# reserve by counts.
for function in CORE_FUNCTIONS:
if cpulists and function in cpulists:
for cpu in cpulists[function]:
node = node_from_cpu(host, cpu)
host.cpu_functions[node][function].append(cpu)
cpu_lists[node].remove(cpu)
for s in range(0, len(host.nodes)):
cpu_list = host.cpu_lists[s] if s in host.cpu_lists else []
# Reserve for the platform first
for i in range(0, cpu_counts[s][constants.PLATFORM_FUNCTION]):
host.cpu_functions[s][constants.PLATFORM_FUNCTION].append(
cpu_list.pop(0))
cpu_lists[s].pop(0))
# Reserve for the vswitch next
for i in range(0, cpu_counts[s][constants.VSWITCH_FUNCTION]):
host.cpu_functions[s][constants.VSWITCH_FUNCTION].append(
cpu_list.pop(0))
cpu_lists[s].pop(0))
# Reserve for the shared next
for i in range(0, cpu_counts[s][constants.SHARED_FUNCTION]):
host.cpu_functions[s][constants.SHARED_FUNCTION].append(
cpu_list.pop(0))
cpu_lists[s].pop(0))
# Reserve for the isolated next
for i in range(0, cpu_counts[s][constants.ISOLATED_FUNCTION]):
host.cpu_functions[s][constants.ISOLATED_FUNCTION].append(
cpu_list.pop(0))
cpu_lists[s].pop(0))
# Assign the remaining cpus to the default function for this host
host.cpu_functions[s][get_default_function(host)] += cpu_list
host.cpu_functions[s][get_default_function(host)] += cpu_lists[s]
return

View File

@ -233,7 +233,8 @@ class HostStatesController(rest.RestController):
Example:
capabilities=[{'function': 'platform', 'sockets': [{'0': 1}, {'1': 0}]},
{'function': 'vswitch', 'sockets': [{'0': 2}]},
{'function': 'shared', 'sockets': [{'0': 1}, {'1': 1}]}]
{'function': 'shared', 'sockets': [{'0': 1}, {'1': 1}]},
{'function': 'application-isolated', 'cpulist': '3-5,6'}]
"""
LOG.info("host_cpus_modify host_uuid=%s capabilities=%s" %
(host_uuid, capabilities))
@ -244,16 +245,28 @@ class HostStatesController(rest.RestController):
ihost.nodes = pecan.request.dbapi.inode_get_by_ihost(ihost.uuid)
num_nodes = len(ihost.nodes)
# Query the database to get the current set of CPUs
ihost.cpus = pecan.request.dbapi.icpu_get_by_ihost(ihost.uuid)
# Perform basic sanity on the input
for icap in capabilities:
specified_function = icap.get('function', None)
specified_sockets = icap.get('sockets', None)
if not specified_function or not specified_sockets:
specified_sockets = icap.get('sockets', [])
specified_cpulist = icap.get('cpulist', None)
if specified_sockets and specified_cpulist:
raise wsme.exc.ClientSideError(
_('host %s: cpu function=%s or socket=%s not specified '
'for host %s.') % (host_uuid,
specified_function,
specified_sockets))
_('host %s: socket=%s and cpulist=%s may not both be specified') %
(host_uuid, specified_sockets, specified_cpulist))
if not specified_function or not (specified_sockets or specified_cpulist):
raise wsme.exc.ClientSideError(
_('host %s: cpu function=%s or (socket=%s and cpulist=%s) '
'not specified') % (host_uuid,
specified_function,
specified_sockets,
specified_cpulist))
for specified_socket in specified_sockets:
socket, value = specified_socket.items()[0]
if int(socket) >= num_nodes:
@ -264,22 +277,35 @@ class HostStatesController(rest.RestController):
raise wsme.exc.ClientSideError(
_('Specified cpu values must be non-negative.'))
# Query the database to get the current set of CPUs and then
# organize the data by socket and function for convenience.
ihost.cpus = pecan.request.dbapi.icpu_get_by_ihost(ihost.uuid)
# Ensure that the cpulist is valid if set
if specified_cpulist:
# make a list of CPU numbers (which are not necessarily contiguous)
host_cpus = [ihost_cpu.cpu for ihost_cpu in ihost.cpus]
cpulist = cutils.parse_range_set(specified_cpulist)
if max(cpulist) > max(host_cpus):
raise wsme.exc.ClientSideError(
_('Specified cpulist contains nonexistant CPUs.'))
# organize the cpus by socket and function for convenience.
cpu_utils.restructure_host_cpu_data(ihost)
# Get the CPU counts for each socket and function for this host
cpu_counts = cpu_utils.get_cpu_counts(ihost)
# Update the CPU counts based on the provided values
cpu_lists = {}
# Update the CPU counts and cpulists based on the provided values
for cap in capabilities:
function = cap.get('function', None)
# Normalize the function input
for const_function in constants.CPU_FUNCTIONS:
if const_function.lower() == function.lower():
function = const_function
sockets = cap.get('sockets', None)
sockets = cap.get('sockets', [])
# If this function is specified via cpulist, reset count to zero.
if not sockets:
for numa_node in cpu_counts:
cpu_counts[numa_node][function] = 0
for numa in sockets:
numa_node, value = numa.items()[0]
numa_node = int(numa_node)
@ -288,11 +314,18 @@ class HostStatesController(rest.RestController):
value *= 2
cpu_counts[numa_node][function] = value
# Store the cpu ranges per CPU function as well if any exist
cpu_range = cap.get('cpulist', None)
cpu_list = cutils.parse_range_set(cpu_range)
# Uncomment the following line to add any missing HT siblings
# cpu_list = cpu_utils.append_ht_sibling(ihost, cpu_list)
cpu_lists[function] = cpu_list
# Semantic check to ensure the minimum/maximum values are enforced
cpu_utils.check_core_allocations(ihost, cpu_counts)
cpu_utils.check_core_allocations(ihost, cpu_counts, cpu_lists)
# Update cpu assignments to new values
cpu_utils.update_core_allocations(ihost, cpu_counts)
cpu_utils.update_core_allocations(ihost, cpu_counts, cpu_lists)
for cpu in ihost.cpus:
function = cpu_utils.get_cpu_function(ihost, cpu)

View File

@ -72,6 +72,8 @@ from oslo_log import log as logging
from fm_api import constants as fm_constants
from six.moves import range
from sysinv._i18n import _
from sysinv.common import exception
from sysinv.common import constants
@ -1731,6 +1733,20 @@ def get_disk_capacity_mib(device_node):
return int(size_mib)
def parse_range_set(range_string):
""" Return a non-sorted list specified by a range string."""
# TODO: add UTs for this.
# Parse a range string as specified by format_range_set() below
# Be generous dealing with duplicate entries in the specification.
if not range_string:
return []
ranges = [
(lambda sublist: range(sublist[0], sublist[-1] + 1))
(list(map(int, subrange.split('-')))) for subrange in range_string.split(',')]
return list(set([y for x in ranges for y in x]))
def format_range_set(items):
# Generate a pretty-printed value of ranges, such as 3-6,8-9,12-17
ranges = []

View File

@ -0,0 +1,38 @@
#
# Copyright (c) 2021 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
"""
Tests for the generic utils.
"""
from sysinv.common import utils
from sysinv.tests import base
class TestCommonUtils(base.TestCase):
def test_parse_range_set(self):
# Empty string
self.assertEqual(utils.parse_range_set(""), [])
# Single item
self.assertEqual(utils.parse_range_set("11"), [11])
# Multi non-consecutive items
self.assertEqual(set(utils.parse_range_set("1,3,5")), set([1, 3, 5]))
# Multi consecutive items
self.assertEqual(set(utils.parse_range_set("1,2,3")), set([1, 2, 3]))
# Out of order
self.assertEqual(set(utils.parse_range_set("1,3,2")), set([1, 2, 3]))
# Single range
self.assertEqual(set(utils.parse_range_set("7-10")),
set([7, 8, 9, 10]))
# Mix of single items and range
self.assertEqual(set(utils.parse_range_set("1,3-7,11,2")),
set([1, 2, 3, 4, 5, 6, 7, 11]))
# Duplicates
self.assertEqual(set(utils.parse_range_set("1,2,3,2,1")),
set([1, 2, 3]))
# Single items overlapping with range
self.assertEqual(set(utils.parse_range_set("1-3,2,1")),
set([1, 2, 3]))