Add ihost cpu_max_frequency

Adds a new host attribute to limit the host CPU frequency
via system command.

Test plan:
PASS: Bootstrap a system.
PASS: Configure and restart sysinv-agent and check configuration
         persistence.
PASS: Configure a value higher than supported by the CPU.
      (cpupower doesn't allow)
PASS: Configure a value below than supported by the CPU.
      (cpupower doesn't allow)
PASS: Upgrade a system.
PASS: Ensure cpupower utility is available on debian OS.
      (linux-cpupower package not present)
      https://review.opendev.org/c/starlingx/tools/+/839205
      adds the package to the debian builds

Story: 2009886
Task: 44882

Depends-On: https://review.opendev.org/c/starlingx/stx-puppet/+/835748

Signed-off-by: Iago Estrela <IagoFilipe.EstrelaBarros@windriver.com>
Change-Id: If9ed56497791f5acd36a28fad5d3c2c7ba11db74
This commit is contained in:
Iago Estrela 2022-03-29 14:42:29 -03:00
parent b8af3ac233
commit 6f47c14cbf
12 changed files with 229 additions and 4 deletions

View File

@ -35,8 +35,8 @@ def _print_ihost_show(ihost, columns=None, output_format=None):
'boot_device', 'rootfs_device', 'install_output', 'console',
'tboot', 'vim_progress_status', 'software_load',
'install_state', 'install_state_info', 'inv_state',
'clock_synchronization',
'device_image_update', 'reboot_needed']
'clock_synchronization', 'device_image_update',
'reboot_needed', 'max_cpu_frequency', 'max_cpu_default']
optional_fields = ['vsc_controllers', 'ttys_dcd']
if ihost.subfunctions != ihost.personality:
fields.append('subfunctions')
@ -687,3 +687,23 @@ def do_host_device_image_update_abort(cc, args):
raise exc.CommandError(
'Device image update-abort failed: host %s' % args.hostnameorid)
_print_ihost_show(host)
@utils.arg('hostnameorid',
metavar='<hostnameorid>',
help="Name or ID of host")
@utils.arg('max_cpu_frequency',
metavar='<max_cpu_frequency>',
help="Max CPU frequency MHz")
def do_host_cpu_max_frequency_modify(cc, args):
"""Modify host cpu max frequency."""
attributes = ['max_cpu_frequency=%s' % args.max_cpu_frequency]
patch = utils.args_array_to_patch("replace", attributes)
ihost = ihost_utils._find_ihost(cc, args.hostnameorid)
try:
ihost = cc.ihost.update(ihost.id, patch)
except exc.HTTPNotFound:
raise exc.CommandError('host not found: %s' % args.hostnameorid)
_print_ihost_show(ihost)

View File

@ -288,6 +288,28 @@ class AgentManager(service.PeriodicService):
else:
LOG.debug("ttys_dcd is not configured")
def _max_cpu_frequency_configurable(self):
fail_result = "System does not support"
output = utils.execute('/usr/bin/cpupower', 'info', run_as_root=True)
if isinstance(output, tuple):
cpu_info = output[0] or ''
if not cpu_info.startswith(fail_result):
return constants.CONFIGURABLE
return constants.NOT_CONFIGURABLE
def _max_cpu_frequency_default(self):
output = utils.execute(
"lscpu | grep 'CPU max MHz' | awk '{ print $4 }' | cut -d ',' -f 1",
shell=True)
if isinstance(output, tuple):
default_max = output[0]
if default_max:
LOG.info("Default CPU max frequency: {}".format(default_max))
return int(default_max.split('.')[0])
@staticmethod
def _get_active_device():
# the list of currently configured console devices,
@ -550,8 +572,14 @@ class AgentManager(service.PeriodicService):
Action State to reinstalled, and remove the flag.
"""
if os.path.exists(FIRST_BOOT_FLAG):
max_cpu_freq_dict = {
constants.IHOST_MAX_CPU_CONFIG:
self._max_cpu_frequency_configurable(),
constants.IHOST_MAX_CPU_DEFAULT:
self._max_cpu_frequency_default()}
msg_dict.update({constants.HOST_ACTION_STATE:
constants.HAS_REINSTALLED})
constants.HAS_REINSTALLED,
'max_cpu_dict': max_cpu_freq_dict})
# Is this the first time since boot we are reporting to conductor?
msg_dict.update({constants.SYSINV_AGENT_FIRST_REPORT:

View File

@ -550,6 +550,12 @@ class Host(base.APIBase):
install_state_info = wtypes.text
"Represent install state extra information if there is any"
max_cpu_frequency = wtypes.text
"Represent the CPU max frequency"
max_cpu_default = wtypes.text
"Represent the default CPU max frequency"
iscsi_initiator_name = wtypes.text
"The iscsi initiator name (only used for worker hosts)"
@ -587,7 +593,8 @@ class Host(base.APIBase):
'install_state', 'install_state_info',
'iscsi_initiator_name',
'device_image_update', 'reboot_needed',
'inv_state', 'clock_synchronization']
'inv_state', 'clock_synchronization',
'max_cpu_frequency', 'max_cpu_default']
fields = minimum_fields if not expand else None
uhost = Host.from_rpc_object(rpc_ihost, fields)
@ -2076,6 +2083,15 @@ class HostController(rest.RestController):
'bm_username': None,
'bm_password': None})
if 'max_cpu_frequency' in delta:
self._check_max_cpu_frequency(hostupdate)
max_cpu_frequency = hostupdate.ihost_patch.get('max_cpu_frequency')
ihost_obj['max_cpu_frequency'] = max_cpu_frequency
pecan.request.dbapi.ihost_update(
ihost_obj['uuid'],
{'max_cpu_frequency': max_cpu_frequency})
hostupdate.configure_required = True
if hostupdate.ihost_val_prenotify:
# update value in db prior to notifications
LOG.info("update ihost_val_prenotify: %s" %
@ -2128,6 +2144,9 @@ class HostController(rest.RestController):
ihost_ret = pecan.request.rpcapi.configure_ihost(
pecan.request.context, ihost_obj)
pecan.request.rpcapi.update_host_max_cpu_frequency(
pecan.request.context, ihost_obj)
pecan.request.dbapi.ihost_update(
ihost_obj['uuid'], {'capabilities': ihost_obj['capabilities']})
@ -2866,6 +2885,31 @@ class HostController(rest.RestController):
"operation can proceed")
% (personality, load.software_version))
def _check_max_cpu_frequency(self, host):
max_cpu_frequency = host.ihost_patch.get('max_cpu_frequency')
if(constants.WORKER in host.ihost_orig[constants.SUBFUNCTIONS] and
host.ihost_orig.get('capabilities').get(constants.IHOST_MAX_CPU_CONFIG) ==
constants.CONFIGURABLE and max_cpu_frequency):
if max_cpu_frequency == constants.IHOST_MAX_CPU_DEFAULT:
max_cpu_default = host.ihost_orig['max_cpu_default']
host.ihost_patch['max_cpu_frequency'] = max_cpu_default
return
if not max_cpu_frequency.lstrip('-+').isdigit():
raise wsme.exc.ClientSideError(
_("Max CPU frequency %s must be an integer")
% (max_cpu_frequency))
if not int(max_cpu_frequency) >= 1:
raise wsme.exc.ClientSideError(
_("Max CPU frequency %s must be greater than 0")
% (max_cpu_frequency))
else:
raise wsme.exc.ClientSideError(
_("Host does not support CPU max frequency"))
def _check_host_load(self, hostname, load):
host = pecan.request.dbapi.ihost_get_by_hostname(hostname)
host_upgrade = objects.host_upgrade.get_by_host_id(

View File

@ -199,6 +199,8 @@ PATCH_DEFAULT_TIMEOUT_IN_SECS = 6
# ihost field attributes
IHOST_STOR_FUNCTION = 'stor_function'
IHOST_MAX_CPU_CONFIG = 'max_cpu_config'
IHOST_MAX_CPU_DEFAULT = 'max_cpu_default'
# ihost config_status field values
CONFIG_STATUS_OUT_OF_DATE = "Config out-of-date"
@ -2052,3 +2054,7 @@ OS_RELEASE_FILE = '/etc/os-release'
OS_CENTOS = 'centos'
OS_DEBIAN = 'debian'
SUPPORTED_OS_TYPES = [OS_CENTOS, OS_DEBIAN]
# Configuration support placeholders
CONFIGURABLE = 'configurable'
NOT_CONFIGURABLE = 'not-configurable'

View File

@ -5332,6 +5332,7 @@ class ConductorManager(service.PeriodicService):
ihost_uuid.strip()
LOG.info("Updating platform data for host: %s "
"with: %s" % (ihost_uuid, imsg_dict))
try:
ihost = self.dbapi.ihost_get(ihost_uuid)
except exception.ServerNotFound:
@ -5339,6 +5340,7 @@ class ConductorManager(service.PeriodicService):
return
availability = imsg_dict.get('availability')
max_cpu_dict = imsg_dict.get('max_cpu_dict')
val = {}
@ -5354,6 +5356,14 @@ class ConductorManager(service.PeriodicService):
(ihost.hostname, iscsi_initiator_name))
val['iscsi_initiator_name'] = iscsi_initiator_name
if max_cpu_dict:
ihost.capabilities.update({
constants.IHOST_MAX_CPU_CONFIG:
max_cpu_dict.get(constants.IHOST_MAX_CPU_CONFIG)})
ihost.max_cpu_default = max_cpu_dict.get('max_cpu_default')
val.update({'capabilities': ihost.capabilities,
constants.IHOST_MAX_CPU_DEFAULT: ihost.max_cpu_default})
if val:
ihost = self.dbapi.ihost_update(ihost_uuid, val)
@ -13223,6 +13233,20 @@ class ConductorManager(service.PeriodicService):
LOG.error(msg)
raise exception.SysinvException(_(msg))
def update_host_max_cpu_frequency(self, context, host):
personalities = [constants.WORKER]
config_uuid = self._config_update_hosts(context,
personalities,
[host['uuid']])
config_dict = {
"personalities": personalities,
"classes": ['platform::compute::config::runtime']
}
self._config_apply_runtime_manifest(context,
config_uuid,
config_dict)
def update_admin_ep_certificate(self, context):
"""
Update admin endpoint certificate

View File

@ -2290,3 +2290,14 @@ class ConductorAPI(sysinv.openstack.common.rpc.proxy.RpcProxy):
certificate_list=certificate_list,
issuers_list=issuers_list,
secret_list=secret_list))
def update_host_max_cpu_frequency(self, context, host):
"""Synchronously, execute runtime manifests to update host max_cpu_frequency.
:param context: request context.
:param ihost: the host to update the max_cpu_frequency.
"""
return self.call(context,
self.make_msg('update_host_max_cpu_frequency',
host=host))

View File

@ -0,0 +1,20 @@
#
# Copyright (c) 2022 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
from sqlalchemy import Column, MetaData, Table
from sqlalchemy import String
def upgrade(migrate_engine):
meta = MetaData()
meta.bind = migrate_engine
host_table = Table('i_host', meta, autoload=True)
host_table.create_column(Column('max_cpu_frequency', String(64)))
host_table.create_column(Column('max_cpu_default', String(64)))
def downgrade(migrate_engine):
raise NotImplementedError('SysInv database downgrade is unsupported.')

View File

@ -239,6 +239,8 @@ class ihost(Base):
device_image_update = Column(String(64))
reboot_needed = Column(Boolean, nullable=False, default=False)
max_cpu_frequency = Column(String(64)) # in MHz
max_cpu_default = Column(String(64)) # in MHz
forisystemid = Column(Integer,
ForeignKey('i_system.id', ondelete='CASCADE'))

View File

@ -100,6 +100,8 @@ class Host(base.SysinvObject):
'iscsi_initiator_name': utils.str_or_none,
'device_image_update': utils.str_or_none,
'reboot_needed': utils.bool_or_none,
'max_cpu_frequency': utils.str_or_none,
'max_cpu_default': utils.str_or_none
}
_foreign_fields = {

View File

@ -634,6 +634,8 @@ class PlatformPuppet(base.BasePuppet):
reserved_vswitch_cores,
'platform::compute::params::reserved_platform_cores':
reserved_platform_cores,
'platform::compute::params::max_cpu_frequency':
host.max_cpu_frequency,
'platform::compute::grub::params::n_cpus': n_cpus,
'platform::compute::grub::params::cpu_options': cpu_options,
})

View File

@ -22,6 +22,7 @@ class FakeConductorAPI(object):
def __init__(self):
self.create_host_filesystems = mock.MagicMock()
self.update_host_max_cpu_frequency = mock.MagicMock()
self.is_virtual_system_config_result = False
def is_virtual_system_config(self, ctxt):

View File

@ -46,6 +46,7 @@ class FakeConductorAPI(object):
self.kube_upgrade_kubelet = mock.MagicMock()
self.create_barbican_secret = mock.MagicMock()
self.mtc_action_apps_semantic_checks = mock.MagicMock()
self.update_host_max_cpu_frequency = mock.MagicMock()
def create_ihost(self, context, values):
# Create the host in the DB as the code under test expects this
@ -3369,3 +3370,67 @@ class RejectMaintananceActionByAppTestCase(TestHost):
self.assertEqual(response.content_type, 'application/json')
self.assertEqual(response.status_code, http_client.OK)
class TestHostModifyCPUMaxFrequency(TestHost):
def test_host_max_cpu_frequency_not_configurable(self):
worker = self._create_worker(
max_cpu_frequency=None,
invprovision=constants.PROVISIONED,
administrative=constants.ADMIN_UNLOCKED,
operational=constants.OPERATIONAL_ENABLED,
availability=constants.AVAILABILITY_ONLINE,
capabilities={constants.IHOST_MAX_CPU_CONFIG:
constants.NOT_CONFIGURABLE})
self.assertRaises(
webtest.app.AppError,
self._patch_host,
worker.get('hostname'),
[{'path': '/max_cpu_frequency',
'value': '283487',
'op': 'replace'}],
'sysinv-test')
def test_host_max_cpu_frequency_configurable_bad_values(self):
worker = self._create_worker(
max_cpu_frequency=None,
invprovision=constants.PROVISIONED,
administrative=constants.ADMIN_UNLOCKED,
operational=constants.OPERATIONAL_ENABLED,
availability=constants.AVAILABILITY_ONLINE,
capabilities={constants.IHOST_MAX_CPU_CONFIG:
constants.CONFIGURABLE})
for bad_value in ['AAAAA', '1A1A1A1', '-1', '0']:
self.assertRaises(
webtest.app.AppError,
self._patch_host,
worker.get('hostname'),
[{'path': '/max_cpu_frequency',
'value': bad_value,
'op': 'replace'}],
'sysinv-test')
def test_host_max_cpu_frequency_default(self):
max_cpu_default = 1000000
worker = self._create_worker(
max_cpu_frequency=None,
max_cpu_default=max_cpu_default,
invprovision=constants.PROVISIONED,
administrative=constants.ADMIN_UNLOCKED,
operational=constants.OPERATIONAL_ENABLED,
availability=constants.AVAILABILITY_ONLINE,
capabilities={constants.IHOST_MAX_CPU_CONFIG:
constants.CONFIGURABLE})
response = self._patch_host(
worker.get('hostname'),
[{'path': '/max_cpu_frequency',
'value': 'max_cpu_default',
'op': 'replace'}],
'sysinv-test')
self.assertEqual(response.content_type, 'application/json')
self.assertEqual(response.status_code, http_client.OK)