From 6f47c14cbfd2f113be8a195f1bc86f1cdcffbdd3 Mon Sep 17 00:00:00 2001 From: Iago Estrela Date: Tue, 29 Mar 2022 14:42:29 -0300 Subject: [PATCH] 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 Change-Id: If9ed56497791f5acd36a28fad5d3c2c7ba11db74 --- .../cgts-client/cgtsclient/v1/iHost_shell.py | 24 ++++++- sysinv/sysinv/sysinv/sysinv/agent/manager.py | 30 ++++++++- .../sysinv/sysinv/api/controllers/v1/host.py | 46 ++++++++++++- .../sysinv/sysinv/sysinv/common/constants.py | 6 ++ .../sysinv/sysinv/sysinv/conductor/manager.py | 24 +++++++ .../sysinv/sysinv/sysinv/conductor/rpcapi.py | 11 ++++ .../versions/124_max_cpu_frequency.py | 20 ++++++ .../sysinv/sysinv/db/sqlalchemy/models.py | 2 + sysinv/sysinv/sysinv/sysinv/objects/host.py | 2 + .../sysinv/sysinv/sysinv/puppet/platform.py | 2 + .../sysinv/sysinv/tests/agent/test_manager.py | 1 + .../sysinv/sysinv/tests/api/test_host.py | 65 +++++++++++++++++++ 12 files changed, 229 insertions(+), 4 deletions(-) create mode 100644 sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/migrate_repo/versions/124_max_cpu_frequency.py diff --git a/sysinv/cgts-client/cgts-client/cgtsclient/v1/iHost_shell.py b/sysinv/cgts-client/cgts-client/cgtsclient/v1/iHost_shell.py index a753cfa579..b83b122c14 100755 --- a/sysinv/cgts-client/cgts-client/cgtsclient/v1/iHost_shell.py +++ b/sysinv/cgts-client/cgts-client/cgtsclient/v1/iHost_shell.py @@ -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='', + help="Name or ID of host") +@utils.arg('max_cpu_frequency', + metavar='', + 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) diff --git a/sysinv/sysinv/sysinv/sysinv/agent/manager.py b/sysinv/sysinv/sysinv/sysinv/agent/manager.py index a2ea8ac864..bf179cd1ef 100644 --- a/sysinv/sysinv/sysinv/sysinv/agent/manager.py +++ b/sysinv/sysinv/sysinv/sysinv/agent/manager.py @@ -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: diff --git a/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/host.py b/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/host.py index 9874daf78c..eca68eddf0 100644 --- a/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/host.py +++ b/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/host.py @@ -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( diff --git a/sysinv/sysinv/sysinv/sysinv/common/constants.py b/sysinv/sysinv/sysinv/sysinv/common/constants.py index 35ea0494c2..85b9f1c8b1 100644 --- a/sysinv/sysinv/sysinv/sysinv/common/constants.py +++ b/sysinv/sysinv/sysinv/sysinv/common/constants.py @@ -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' diff --git a/sysinv/sysinv/sysinv/sysinv/conductor/manager.py b/sysinv/sysinv/sysinv/sysinv/conductor/manager.py index 5af0dbcafd..141444c334 100644 --- a/sysinv/sysinv/sysinv/sysinv/conductor/manager.py +++ b/sysinv/sysinv/sysinv/sysinv/conductor/manager.py @@ -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 diff --git a/sysinv/sysinv/sysinv/sysinv/conductor/rpcapi.py b/sysinv/sysinv/sysinv/sysinv/conductor/rpcapi.py index 8312fd5972..7013129a95 100644 --- a/sysinv/sysinv/sysinv/sysinv/conductor/rpcapi.py +++ b/sysinv/sysinv/sysinv/sysinv/conductor/rpcapi.py @@ -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)) diff --git a/sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/migrate_repo/versions/124_max_cpu_frequency.py b/sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/migrate_repo/versions/124_max_cpu_frequency.py new file mode 100644 index 0000000000..c52c0d7fab --- /dev/null +++ b/sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/migrate_repo/versions/124_max_cpu_frequency.py @@ -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.') diff --git a/sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/models.py b/sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/models.py index b811cd2b15..1a3c42d5d7 100644 --- a/sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/models.py +++ b/sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/models.py @@ -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')) diff --git a/sysinv/sysinv/sysinv/sysinv/objects/host.py b/sysinv/sysinv/sysinv/sysinv/objects/host.py index 626ba1d4a8..58fe8abda9 100644 --- a/sysinv/sysinv/sysinv/sysinv/objects/host.py +++ b/sysinv/sysinv/sysinv/sysinv/objects/host.py @@ -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 = { diff --git a/sysinv/sysinv/sysinv/sysinv/puppet/platform.py b/sysinv/sysinv/sysinv/sysinv/puppet/platform.py index 6dc04e890a..593a260ee1 100644 --- a/sysinv/sysinv/sysinv/sysinv/puppet/platform.py +++ b/sysinv/sysinv/sysinv/sysinv/puppet/platform.py @@ -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, }) diff --git a/sysinv/sysinv/sysinv/sysinv/tests/agent/test_manager.py b/sysinv/sysinv/sysinv/sysinv/tests/agent/test_manager.py index 14772624da..05248c5961 100644 --- a/sysinv/sysinv/sysinv/sysinv/tests/agent/test_manager.py +++ b/sysinv/sysinv/sysinv/sysinv/tests/agent/test_manager.py @@ -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): diff --git a/sysinv/sysinv/sysinv/sysinv/tests/api/test_host.py b/sysinv/sysinv/sysinv/sysinv/tests/api/test_host.py index 0e965ca7c4..6e6a9352f2 100644 --- a/sysinv/sysinv/sysinv/sysinv/tests/api/test_host.py +++ b/sysinv/sysinv/sysinv/sysinv/tests/api/test_host.py @@ -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)