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:
parent
6c51808ffd
commit
4cc0fe7332
|
@ -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',
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 = []
|
||||
|
|
|
@ -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]))
|
Loading…
Reference in New Issue