add sysinv support for specifying cpu function by range

In order to add flexibility we want to allow specifying CPU
function by range, rather than just by count.

This will allow us to run something like this on the CLI:

system host-cpu-modify -f application-isolated -c 3-5,25 controller-0

There are a couple complications to be aware of.  First, sysinv will
NOT automatically add any missing SMT hyperthreads if the host has
hyperthreading enabled.  Second, when specifying CPU function for
a different function via the CLI the range specification is lost and
gets converted to a simple count.  This implies that (for the CLI)
only one function can support a range-based specification, and it
must be specified last.

Story: 2008760
Task: 42180
Change-Id: Id21d9968b6b0b59e163f42098be7a6f0e6ef739d
Signed-off-by: Chris Friesen <chris.friesen@windriver.com>
This commit is contained in:
Chris Friesen 2020-10-24 17:23:00 -06:00
parent 6c51808ffd
commit 4cc0fe7332
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]))