Merge "add sysinv support for specifying cpu function by range"
This commit is contained in:
commit
7ce5367d57
@ -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 = []
|
||||
|
38
sysinv/sysinv/sysinv/sysinv/tests/common/test_utils.py
Normal file
38
sysinv/sysinv/sysinv/sysinv/tests/common/test_utils.py
Normal 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]))
|
Loading…
x
Reference in New Issue
Block a user