system host-label support stalld labels

update label unit tests
modify system host-label cgtsclient
add semantic checks to api/label.py for stalld
 - check valid label values for stalld labels
 - check cpu_list when enabling stalld service
   / modifying cpu_functions label
update v1/label rest api Unit tests for stalld
add configure_stalld() to rpcapi and conductor
add stalld_config() to puppet Platform Plugin

Document stalld label assignments
update stalld semantic check to be case insensitive
but case agnostic when saving to the label database

Added semantic check for custom stalld label in
sysinv api layer

Added new label_get_all_like sql api to query
  all labels that match the specific pattern

UT for stalld discontinous cpu ranges and
multiple cpu threads

Added unit tests for custom stalld parameters

E.g

  system host-label-assign
  system host-label-assign \
  <node> \
  starlingx.io/stalld=enabled \
  starlingx.io/stalld_cpu_functions=all \
  starlingx.io/stalld.boost_period=100000

For parameters that do not have any values the
user can provide any string including the empty
string as the label value

In order to get parameter:
  stalld --aggressive_mode

  system host-label-assign \
  <node> starlingx.io/stalld.aggressive_mode=''

  system host-label-remove \
  <node> starlingx.io/stalld.aggressive_mode

Test plan:

Verify documentation - restview <rst file>

PASS - cgts-client Unit testing
PASS - test_label.py Unit testing

PASS - AIO-SX: iso install

PASS - Semantic checking tests
       - worker nodes only AIO & worker
       - invalid label values
       - empty cpu list e.g isolated cpus not
         assigned but set for stalld

PASS - enable/disable stalld service
       using host label
       verify status of stalld service
PASS - change stalld cpu functions label
        [all | application-isolated| application]
        verify cpu list being modified by stalld
        changes

PASS - host-lock/host-unlock controller AIO-SX

PASS - switch to rt kernel verify stalld

PASS - host reboot AIO-SX

PASS - host-cpu-modify ( change core assigments)
       verify stalld labels get updated after
       host-unlock reboot

PASS - AIO-DX iso install
              host-swact with stalld running
              verify running after swact

PASS: - AIO-SX system with 32 cores and 2 threads per core
      - cpus 2-31 and 34-63 reserved for Application
      - stalld_cpu_functions label set for Application
      - cpus 2-31,34-63 are monitored by stalld

PASS - AIO-SX: add custom stalld parameter and
               verify hieradata
               verify stalld startup parameters

Story: 2011378
Task: 52174

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

Change-Id: Ie718c5927b457da49c693d184a5c16cc73912de3
Signed-off-by: Kyale, Eliud <Eliud.Kyale@windriver.com>
This commit is contained in:
Kyale, Eliud
2025-03-24 13:50:57 -04:00
parent 19e720b270
commit e26d0ef26e
15 changed files with 1718 additions and 27 deletions

View File

@ -6931,6 +6931,146 @@ Deletes a device label
This operation does not accept a request body.
--------------
Host labels
--------------
************************
List all the host labels
************************
.. rest_method:: GET /v1/ihosts/{ihost_uuid}/labels
**Normal response codes**
200
**Error response codes**
computeFault (400, 500, ...), serviceUnavailable (503), badRequest (400),
unauthorized (401), forbidden (403), badMethod (405), overLimit (413),
itemNotFound (404)
**Response parameters**
.. csv-table::
:header: "Parameter", "Style", "Type", "Description"
:widths: 20, 20, 20, 60
"labels ", "plain", "xsd:list", "The list of host labels."
"uuid ", "plain", "csapi:UUID", "The universally unique identifier for this object."
"label_key ", "plain", "xsd:string", "The key of the device label."
"label_value ", "plain", "xsd:string", "The value of the device label."
"host_uuid ", "plain", "csapi:UUID", "The universally unique identifier for the host object."
::
{
"labels": [
{
"uuid": "71caa220-390f-4403-86a3-8061dba35d06",
"label_key": "key1",
"label_value": "value1",
"host_uuid": "960c759f-fc00-42a1-b67e-a796bf709258"
},
{
"uuid": "4512b32f-943a-48d0-9449-9119205302c2",
"label_key": "key5",
"label_value": "value5",
"host_uuid": "960c759f-fc00-42a1-b67e-a796bf709258"
}
]
}
******************************
Assign host labels to a host
******************************
.. rest_method:: POST /v1/labels/{ihost_uuid}?overwrite={overwrite_parameter}
**Normal response codes**
200
**Error response codes**
computeFault (400, 500, ...), serviceUnavailable (503), badRequest (400),
unauthorized (401), forbidden (403), badMethod (405), overLimit (413),
itemNotFound (404)
**Request parameters**
.. csv-table::
:header: "Parameter", "Style", "Type", "Description"
:widths: 20, 20, 20, 60
"ihost_uuid ", "plain", "csapi:UUID", "The universally unique identifier for the host object."
"overwrite_parameter (Optional)", "plain", "xsd:boolean", "Overwrite label if it already exists."
"host_labels", "URI", "xsd:list", "List of key-value paired of device labels."
::
{
"key1": "value1",
"key2": "value2",
"key3": "value3"
}
**Response parameters**
.. csv-table::
:header: "Parameter", "Style", "Type", "Description"
:widths: 20, 20, 20, 60
"labels ", "plain", "xsd:list", "The list of host labels."
"uuid ", "plain", "csapi:UUID", "The universally unique identifier for this object."
"label_key ", "plain", "xsd:string", "The key of the device label."
"label_value ", "plain", "xsd:string", "The value of the device label."
"host_uuid ", "plain", "csapi:UUID", "The universally unique identifier for the host object."
::
{
"labels": [
{
"uuid": "bfb37f67-d231-4bf2-836b-677c9cd04dd6",
"label_key": "key1",
"label_value": "value1",
"host_uuid": "960c759f-fc00-42a1-b67e-a796bf709258"
},
{
"uuid": "85acec16-a163-4ed3-9e24-005602979cd6",
"label_key": "key2",
"label_value": "value2",
"host_uuid": "960c759f-fc00-42a1-b67e-a796bf709258"
},
{
"uuid": "45fb9fff-5b32-4088-ad65-acc191fcd8b2",
"label_key": "key3",
"label_value": "value3",
"host_uuid": "960c759f-fc00-42a1-b67e-a796bf709258"
}
]
}
************************
Delete a host label
************************
.. rest_method:: DELETE /v1/labels/{host_label_uuid}
**Normal response codes**
204
**Request parameters**
.. csv-table::
:header: "Parameter", "Style", "Type", "Description"
:widths: 20, 20, 20, 60
"host_label_uuid", "URI", "csapi:UUID", "The unique identifier of the host label."
------------------
Service Parameter
------------------

View File

@ -0,0 +1,216 @@
#
# Copyright (c) 2025 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
import testtools
import uuid
from cgtsclient.tests import utils
import cgtsclient.v1.label
CONTROLLER_0 = {
'uuid': str(uuid.uuid4()),
'hostname': 'controller-0',
'id': '0',
}
LABEL_1 = {
'uuid': str(uuid.uuid4()),
'host_uuid': CONTROLLER_0['uuid'],
'label_key': 'key1',
'label_value': 'value1',
}
LABEL_2 = {
'uuid': str(uuid.uuid4()),
'host_uuid': CONTROLLER_0['uuid'],
'label_key': 'key2',
'label_value': 'value2',
}
LABEL_3 = {
'uuid': str(uuid.uuid4()),
'host_uuid': CONTROLLER_0['uuid'],
'label_key': 'key3',
'label_value': 'value3',
}
LABEL_4 = {
'uuid': str(uuid.uuid4()),
'host_uuid': CONTROLLER_0['uuid'],
'label_key': 'key4',
'label_value': 'value4',
}
LABELS = {
'labels': [LABEL_1, LABEL_2]
}
NEW_LABELS = {
'labels': [LABEL_3, LABEL_4]
}
OVERWRITE_PARAMETER = "overwrite=" + str(True)
fixtures_default = {
'/v1/labels':
{
'GET': (
{},
LABELS,
),
},
f'/v1/labels/{LABEL_1["uuid"]}':
{
'GET': (
{},
LABEL_1,
),
'DELETE': (
{},
None,
)
},
f'/v1/labels/{LABEL_2["uuid"]}':
{
'GET': (
{},
LABEL_2,
),
'DELETE': (
{},
None,
)
},
f'/v1/labels/{LABEL_3["uuid"]}':
{
'GET': (
{},
LABEL_3,
),
'DELETE': (
{},
None,
)
},
f'/v1/labels/{LABEL_4["uuid"]}':
{
'GET': (
{},
LABEL_4,
),
'DELETE': (
{},
None,
)
},
f'/v1/ihosts/{CONTROLLER_0["uuid"]}/labels':
{
'GET': (
{},
LABELS
),
},
f'/v1/labels/{CONTROLLER_0["uuid"]}?{OVERWRITE_PARAMETER}':
{
'POST': (
{},
NEW_LABELS,
)
}
}
class KubernetesLabelManagerTest(testtools.TestCase):
def setUp(self):
super(KubernetesLabelManagerTest, self).setUp()
self.api = utils.FakeAPI(fixtures_default)
self.mgr = \
cgtsclient.v1.label.KubernetesLabelManager(self.api)
def test_host_label_list(self):
host_uuid = CONTROLLER_0['uuid']
labels = self.mgr.list(host_uuid)
expect = [
# (method, url, headers, body )
(
'GET',
f'/v1/ihosts/{host_uuid}/labels',
{},
None
)
]
self.assertEqual(self.api.calls, expect)
self.assertEqual(len(labels), 2)
def test_host_label_get(self):
label_id = LABEL_1['uuid']
label = self.mgr.get(label_id)
expect = [
# (method, url, headers, body )
(
'GET',
f'/v1/labels/{label_id}',
{},
None,
)
]
self.assertEqual(self.api.calls, expect)
self.assertTrue(
isinstance(label,
cgtsclient.v1.label.KubernetesLabel))
self.assertEqual(label.uuid, LABEL_1['uuid'])
self.assertEqual(label.host_uuid, LABEL_1['host_uuid'])
self.assertEqual(label.label_key, LABEL_1['label_key'])
self.assertEqual(label.label_value, LABEL_1['label_value'])
def test_host_label_assign(self):
keyvaluepairs = {
LABEL_3['label_key']: LABEL_3['label_value'],
LABEL_4['label_key']: LABEL_4['label_value'],
}
host_uuid = CONTROLLER_0['uuid']
assigned_labels = self.mgr.assign(host_uuid,
keyvaluepairs,
[OVERWRITE_PARAMETER])
expect = [
# (method, url, headers, body )
(
'POST',
f'/v1/labels/{host_uuid}?{OVERWRITE_PARAMETER}',
{},
keyvaluepairs,
)
]
self.assertEqual(self.api.calls, expect)
for label in assigned_labels:
self.assertTrue(
isinstance(label, cgtsclient.v1.label.KubernetesLabel)
)
expected_labels = [
cgtsclient.v1.label.KubernetesLabel(None, LABEL_3, True),
cgtsclient.v1.label.KubernetesLabel(None, LABEL_4, True),
]
self.assertEqual(expected_labels, assigned_labels)
def test_host_label_remove(self):
label_uuid = LABEL_4['uuid']
label = self.mgr.remove(label_uuid)
expect = [
# (method, url, headers, body )
(
'DELETE',
f'/v1/labels/{label_uuid}',
{},
None,
)
]
self.assertEqual(self.api.calls, expect)
self.assertTrue(label is None)

View File

@ -0,0 +1,171 @@
#
# Copyright (c) 2025 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
import mock
import uuid
import yaml
from cgtsclient import exc
from cgtsclient.tests import test_shell
from cgtsclient.v1.ihost import ihost
from cgtsclient.v1.label import KubernetesLabel
from testtools import ExpectedException
FAKE_HOST_CONTROLLER_0 = {
'uuid': str(uuid.uuid4()),
'hostname': 'controller-0',
'id': '0',
}
FAKE_HOST_LABEL_1 = {
'uuid': str(uuid.uuid4()),
'host_uuid': FAKE_HOST_CONTROLLER_0['uuid'],
'hostname': FAKE_HOST_CONTROLLER_0['hostname'],
'label_key': 'testkey1',
'label_value': 'testvalue1',
}
FAKE_HOST_LABEL_2 = {
'uuid': str(uuid.uuid4()),
'host_uuid': FAKE_HOST_CONTROLLER_0['uuid'],
'hostname': FAKE_HOST_CONTROLLER_0['hostname'],
'label_key': 'testkey2',
'label_value': 'testvalue2',
}
class KubernetesLabelTest(test_shell.ShellTest):
def setUp(self):
super(KubernetesLabelTest, self).setUp()
self.make_env()
def tearDown(self):
super(KubernetesLabelTest, self).tearDown()
@mock.patch('cgtsclient.v1.label.KubernetesLabelManager.list')
@mock.patch('cgtsclient.v1.ihost._find_ihost')
def test_host_label_list(self, mock_find_ihost, mock_label_list):
mock_find_ihost.return_value = ihost(None, FAKE_HOST_CONTROLLER_0, True)
mock_label_list.return_value = [
KubernetesLabel(None, FAKE_HOST_LABEL_1, True),
KubernetesLabel(None, FAKE_HOST_LABEL_2, True),
]
results_str = self.shell("host-label-list --format yaml controller-0")
results_list = yaml.safe_load(results_str)
self.assertTrue(isinstance(results_list, list),
"host-label-list should return a list")
returned_keys = [
'hostname',
'label_key',
'label_value',
]
lbl_1 = {
k: v for k, v in FAKE_HOST_LABEL_1.items() if k in returned_keys
}
lbl_2 = {
k: v for k, v in FAKE_HOST_LABEL_2.items() if k in returned_keys
}
expected_labels = [lbl_1, lbl_2]
self.assertListEqual(expected_labels, results_list)
@mock.patch('cgtsclient.v1.ihost.ihostManager.list')
def test_host_label_list_host_not_found(self, mock_ihost_list,):
mock_ihost_list.return_value = [
ihost(None, FAKE_HOST_CONTROLLER_0, True)
]
hostname = "controller-1"
exception_str = f"host not found: {hostname}"
with ExpectedException(exc.CommandError, exception_str):
self.shell(f"host-label-list {hostname}")
@mock.patch('cgtsclient.v1.label.KubernetesLabelManager.get')
@mock.patch('cgtsclient.v1.label.KubernetesLabelManager.assign')
@mock.patch('cgtsclient.v1.ihost._find_ihost')
def test_host_label_assign(self, mock_find_ihost, mock_label_assign, mock_label_get):
mock_find_ihost.return_value = ihost(None, FAKE_HOST_CONTROLLER_0, True)
mock_label_assign.return_value = [
KubernetesLabel(None, FAKE_HOST_LABEL_1, True),
KubernetesLabel(None, FAKE_HOST_LABEL_2, True),
]
mock_label_get.side_effect = [
KubernetesLabel(None, FAKE_HOST_LABEL_1, True),
KubernetesLabel(None, FAKE_HOST_LABEL_2, True),
]
hostname = "controller-0"
parameters = \
f" {FAKE_HOST_LABEL_1['label_key']}"\
f"={FAKE_HOST_LABEL_1['label_value']}"\
f" {FAKE_HOST_LABEL_2['label_key']}"\
f"={FAKE_HOST_LABEL_2['label_value']}"
self.shell(f"host-label-assign {hostname} {parameters}")
@mock.patch('cgtsclient.v1.ihost.ihostManager.list')
def test_host_label_assign_host_not_found(self, mock_ihost_list):
mock_ihost_list.return_value = [
ihost(None, FAKE_HOST_CONTROLLER_0, True)
]
hostname = "controller-1"
exception_str = f'host not found: {hostname}'
with ExpectedException(exc.CommandError, exception_str):
self.shell(f"host-label-assign {hostname} newkey=newvalue")
@mock.patch('cgtsclient.v1.label.KubernetesLabelManager.remove')
@mock.patch('cgtsclient.v1.label.KubernetesLabelManager.list')
@mock.patch('cgtsclient.v1.ihost._find_ihost')
def test_host_label_remove(self,
mock_find_ihost,
mock_label_list,
mock_label_remove):
mock_find_ihost.return_value = ihost(None, FAKE_HOST_CONTROLLER_0, True)
mock_label_list.return_value = [
KubernetesLabel(None, FAKE_HOST_LABEL_1, True),
KubernetesLabel(None, FAKE_HOST_LABEL_2, True),
]
hostname = 'controller-0'
key1 = FAKE_HOST_LABEL_1['label_key']
key2 = FAKE_HOST_LABEL_2['label_key']
self.shell(f"host-label-remove {hostname} {key1} {key2}")
label1_uuid = FAKE_HOST_LABEL_1['uuid']
label2_uuid = FAKE_HOST_LABEL_2['uuid']
mock_label_remove.assert_has_calls(
[
mock.call(label1_uuid),
mock.call(label2_uuid),
]
)
@mock.patch('cgtsclient.v1.ihost.ihostManager.list')
def test_host_label_remove_host_not_found(self, mock_ihost_list):
mock_ihost_list.return_value = [
ihost(None, FAKE_HOST_CONTROLLER_0, True)
]
hostname = "controller-1"
exception_str = f'host not found: {hostname}'
with ExpectedException(exc.CommandError, exception_str):
self.shell(f"host-label-remove {hostname} key1")
@mock.patch('cgtsclient.v1.label.KubernetesLabelManager.remove')
@mock.patch('cgtsclient.v1.label.KubernetesLabelManager.list')
@mock.patch('cgtsclient.v1.ihost._find_ihost')
def test_host_label_remove_label_not_found(self,
mock_find_ihost,
mock_label_list,
mock_label_remove):
mock_find_ihost.return_value = ihost(None, FAKE_HOST_CONTROLLER_0, True)
mock_label_list.return_value = [
KubernetesLabel(None, FAKE_HOST_LABEL_1, True),
KubernetesLabel(None, FAKE_HOST_LABEL_2, True),
]
hostname = "controller-0"
self.shell(f"host-label-remove {hostname} unknownkey")
mock_label_remove.assert_not_called()

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2018 Wind River Systems, Inc.
# Copyright (c) 2018,2025 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
@ -35,8 +35,28 @@ class KubernetesLabelManager(base.Manager):
except IndexError:
return None
def _assign(self, url, body):
_, body = self.api.json_request('POST', url, body=body)
if not body:
return None
body = body.get('labels', body)
if not isinstance(body, list):
return self.resource_class(self, body) # noqa pylint: disable=not-callable
resources = []
for item in body:
resources.append(self.resource_class(self, item)) # noqa pylint: disable=not-callable
return resources
def assign(self, host_uuid, label, parameters=None):
return self._create(options.build_url(self._path(host_uuid), q=None, params=parameters), label)
url = options.build_url(
self._path(host_uuid),
q=None,
params=parameters)
return self._assign(url, label)
def remove(self, uuid):
return self._delete(self._path(uuid))

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2018 Wind River Systems, Inc.
# Copyright (c) 2018,2025 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
@ -23,15 +23,20 @@ def _print_label_show(obj):
@utils.arg('hostnameorid',
metavar='<hostname or id>',
help="Name or ID of host [REQUIRED]")
@utils.arg('--format',
choices=['table', 'yaml', 'value'],
help="specify the output format, defaults to table")
def do_host_label_list(cc, args):
"""List kubernetes labels assigned to a host."""
"""List labels assigned to a host."""
ihost = ihost_utils._find_ihost(cc, args.hostnameorid)
host_label = cc.label.list(ihost.uuid)
for i in host_label[:]:
setattr(i, 'hostname', ihost.hostname)
field_labels = ['hostname', 'label key', 'label value']
fields = ['hostname', 'label_key', 'label_value']
utils.print_list(host_label, fields, field_labels, sortby=1)
utils.print_list(host_label, fields,
field_labels, sortby=1,
output_format=args.format)
@utils.arg('hostnameorid',
@ -52,13 +57,13 @@ def do_host_label_assign(cc, args):
ihost = ihost_utils._find_ihost(cc, args.hostnameorid)
parameters = ["overwrite=" + str(args.overwrite)]
new_labels = cc.label.assign(ihost.uuid, attributes, parameters)
for p in new_labels.labels:
uuid = p['uuid']
if uuid is not None:
for lbl in new_labels:
if lbl.uuid is not None:
try:
label_obj = cc.label.get(uuid)
label_obj = cc.label.get(lbl.uuid)
except exc.HTTPNotFound:
raise exc.CommandError('Host label not found: %s' % uuid)
raise exc.CommandError(f'Host label not found: {lbl.uuid}')
_print_label_show(label_obj)

View File

@ -1,4 +1,4 @@
# Copyright (c) 2018-2024 Wind River Systems, Inc.
# Copyright (c) 2018-2024,2025 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
@ -7,10 +7,12 @@ import json
import os
import pecan
from pecan import rest
import re
import wsme
import wsmeext.pecan as wsme_pecan
from wsme import types as wtypes
from contextlib import suppress
from oslo_log import log
from oslo_utils import excutils
from sysinv._i18n import _
@ -152,16 +154,20 @@ class LabelController(rest.RestController):
sort_key=sort_key,
sort_dir=sort_dir)
def _apply_manifest_after_label_operation(self, uuid, keys):
def _apply_manifest_after_label_operation(self, host_uuid, keys):
if (common.LABEL_DISABLE_NOHZ_FULL in keys or
constants.KUBE_POWER_MANAGER_LABEL in keys):
pecan.request.rpcapi.update_grub_config(
pecan.request.context, uuid)
pecan.request.context, host_uuid)
if constants.KUBE_POWER_MANAGER_LABEL in keys:
pecan.request.rpcapi.configure_power_manager(
pecan.request.context)
if _check_if_stalld_label_modified(keys):
pecan.request.rpcapi.configure_stalld(
pecan.request.context, host_uuid)
@wsme_pecan.wsexpose(LabelCollection, types.uuid, types.uuid,
int, wtypes.text, wtypes.text)
def get_all(self, uuid=None, marker=None, limit=None,
@ -203,14 +209,14 @@ class LabelController(rest.RestController):
@cutils.synchronized(LOCK_NAME)
@wsme_pecan.wsexpose(LabelCollection, types.uuid, types.boolean,
body=types.apidict)
def post(self, uuid, overwrite=False, body=None):
def post(self, host_uuid, overwrite=False, body=None):
"""Assign label(s) to a host.
"""
if self._from_ihosts:
raise exception.OperationNotPermitted
LOG.info("patch_data: %s" % body)
host = objects.host.get_by_uuid(pecan.request.context, uuid)
host = objects.host.get_by_uuid(pecan.request.context, host_uuid)
# Probably will never happen, but add an extra guard to be absolutely
# sure no null values make it into the database.
@ -226,13 +232,13 @@ class LabelController(rest.RestController):
_semantic_check_k8s_plugins_labels(host, body)
_semantic_check_stalld_labels(host, body)
existing_labels = {}
for label_key in body.keys():
label = None
try:
with suppress(exception.HostLabelNotFoundByKey):
label = pecan.request.dbapi.label_query(host.id, label_key)
except exception.HostLabelNotFoundByKey:
pass
if label:
if overwrite:
existing_labels.update({label_key: label.uuid})
@ -268,7 +274,7 @@ class LabelController(rest.RestController):
label_uuid = existing_labels.get(key)
new_label = pecan.request.dbapi.label_update(label_uuid, {'label_value': value})
else:
new_label = pecan.request.dbapi.label_create(uuid, values)
new_label = pecan.request.dbapi.label_create(host_uuid, values)
new_records.append(new_label)
except exception.HostLabelAlreadyExists:
# We should not be here
@ -277,11 +283,11 @@ class LabelController(rest.RestController):
try:
vim_api.vim_host_update(
None,
uuid,
host_uuid,
host.hostname,
constants.VIM_DEFAULT_TIMEOUT_IN_SECS)
self._apply_manifest_after_label_operation(
uuid, body.keys())
host_uuid, body.keys())
except Exception as e:
LOG.warn(_("No response vim_api host:%s e=%s" %
(host.hostname, e)))
@ -390,6 +396,154 @@ def evaluate_case_agnostic_policy_name(policy_name, policy_value):
_case_agnostic_check(policy_value, constants.KUBE_CPU_MEMORY_MANAGER_VALUES, policy_name)
def _check_if_stalld_label_modified(keys: list[str]) -> bool:
# check for starlingx.io/stalld substring in any of the labels
return any(constants.LABEL_STALLD in label_key for label_key in keys)
def _is_stalld_enabled(host, body: dict) -> bool:
def _is_enabled_in_db(host) -> tuple[bool, bool]:
# default value if missing from db
found_in_db, stalld_enabled = False, constants.LABEL_VALUE_STALLD_DISABLED
with suppress(exception.HostLabelNotFoundByKey):
label_key = constants.LABEL_STALLD
label = pecan.request.dbapi.label_query(host.id, label_key)
stalld_enabled = label.label_value
found_in_db = True
return found_in_db, (stalld_enabled == constants.LABEL_VALUE_STALLD_ENABLED)
def _is_enabled_in_body(body: dict) -> tuple[bool, bool]:
label_key = constants.LABEL_STALLD
found = label_key in body.keys()
stalld_enabled = body.get(label_key, constants.LABEL_VALUE_STALLD_DISABLED)
return found, (stalld_enabled == constants.LABEL_VALUE_STALLD_ENABLED)
found_in_body, stalld_enabled_in_body = _is_enabled_in_body(body)
found_in_db, stalld_enabled_in_db = _is_enabled_in_db(host)
if found_in_body:
# user input highest priority
return stalld_enabled_in_body
elif found_in_db:
# check database
return stalld_enabled_in_db
# otherwise use the default value 'disabled'
return False
def _semantic_check_stalld_cpu_list_is_not_empty(host, body: dict) -> None:
""" Check that stalld will not be started with an empty cpu list.
Args:
host: host
body (dict): dictionary of label key values being updated
"""
keys = body.keys()
if not _check_if_stalld_label_modified(keys):
return None
if not _is_stalld_enabled(host, body):
return None
label_key = constants.LABEL_STALLD_CPU_FUNCTIONS
supported_values = constants.VALID_STALLD_CPU_FUNCTION_VALUES
cpu_function = body.get(label_key, None)
if not cpu_function:
# default value if missing from db
cpu_function = constants.LABEL_VALUE_CPU_DEFAULT
with suppress(exception.HostLabelNotFoundByKey):
label_key = constants.LABEL_STALLD_CPU_FUNCTIONS
label = pecan.request.dbapi.label_query(host.id, label_key)
cpu_function = label.label_value
if cpu_function.lower() == constants.LABEL_VALUE_CPU_ALL:
# all cpus - assume there will be at least 1 cpu on the node
return None
if cpu_function.lower() == constants.LABEL_VALUE_CPU_APPLICATION_ISOLATED:
functions = {
constants.ISOLATED_FUNCTION
}
elif cpu_function.lower() == constants.LABEL_VALUE_CPU_APPLICATION:
functions = {
constants.APPLICATION_FUNCTION,
constants.ISOLATED_FUNCTION
}
else:
# should not get here
msg = \
f"{host.hostname} invalid value {label_key}={cpu_function}. "\
f"Supported values are {supported_values}"
LOG.error(msg)
raise wsme.exc.ClientSideError(_(msg))
cpu_count = 0
for cpu in pecan.request.dbapi.icpu_get_by_ihost(host.uuid):
if cpu.allocated_function in functions or not functions:
cpu_count += 1
if cpu_count == 0:
msg = \
f"{host.hostname} stalld cpu list empty,"\
f" update cpu core assignments for {functions}"
raise wsme.exc.ClientSideError(_(msg))
def _semantic_check_custom_stalld_label_format(host, body: dict) -> None:
"""Check that custom stalld labels have the correct format
starlingx.io/stalld.<paramater>=<value>
"""
keys = body.keys()
stalld_labels = [label for label in keys if constants.LABEL_STALLD in label]
for label_key in stalld_labels:
# skip supported labels they are validated elsewhere
if label_key in constants.SUPPORTED_STALLD_LABELS:
continue
if not re.match(constants.REGEX_STALLD_CUSTOM_LABEL, label_key):
msg = \
f"{host.hostname} '{label_key}' invalid format,"\
f" format example 'starlingx.io/stalld.<paramater>'"
raise wsme.exc.ClientSideError(_(msg))
def _semantic_check_stalld_labels(host, body: dict) -> None:
"""
Perform semantic checks to ensure the stalld labels have supported values
"""
keys = body.keys()
stalld_labels = [label for label in keys if constants.LABEL_STALLD in label]
if not stalld_labels:
return None
if constants.WORKER not in host.subfunctions:
msg = f"{stalld_labels} can only be modified on worker nodes."
raise wsme.exc.ClientSideError(_(msg))
# tuple format ( label_key, supported_values)
label_tuples = [
# starlingx.io/stalld=[enabled|disabled]
(constants.LABEL_STALLD,
constants.VALID_STALLD_VALUES),
# starlingx.io/stalld_cpu_functions=[all|application|application_isolated]
(constants.LABEL_STALLD_CPU_FUNCTIONS,
constants.VALID_STALLD_CPU_FUNCTION_VALUES)]
for (label_key, supported_values) in label_tuples:
label_value = body.get(label_key, None)
if label_value:
label_value = label_value.lower()
if label_value and label_value not in supported_values:
msg = \
f"{host.hostname} invalid value {label_key}={label_value}. "\
f"Supported values are {supported_values}"
raise wsme.exc.ClientSideError(_(msg))
_semantic_check_stalld_cpu_list_is_not_empty(host, body)
_semantic_check_custom_stalld_label_format(host, body)
def _semantic_check_worker_labels(body):
"""
Perform semantic checks to ensure the worker labels are valid.

View File

@ -2809,3 +2809,40 @@ DEPLOYED = "deployed"
DEPLOYING = "deploying"
REMOVING = "removing"
UNAVAILABLE = "unavailable"
# stalld labels
LABEL_STALLD = 'starlingx.io/stalld'
LABEL_VALUE_STALLD_ENABLED = 'enabled'
LABEL_VALUE_STALLD_DISABLED = 'disabled'
# Default is 'disabled'
VALID_STALLD_VALUES = [
LABEL_VALUE_STALLD_DISABLED,
LABEL_VALUE_STALLD_ENABLED
]
# stalld cpu functions values
LABEL_STALLD_CPU_FUNCTIONS = 'starlingx.io/stalld_cpu_functions'
LABEL_VALUE_CPU_ALL = 'all'
LABEL_VALUE_CPU_APPLICATION = 'application'
LABEL_VALUE_CPU_APPLICATION_ISOLATED = 'application-isolated'
# Default is 'application'
LABEL_VALUE_CPU_DEFAULT = LABEL_VALUE_CPU_APPLICATION
VALID_STALLD_CPU_FUNCTION_VALUES = [
LABEL_VALUE_CPU_APPLICATION,
LABEL_VALUE_CPU_APPLICATION_ISOLATED,
LABEL_VALUE_CPU_ALL
]
SUPPORTED_STALLD_LABELS = [
LABEL_STALLD,
LABEL_STALLD_CPU_FUNCTIONS
]
# Custom arguments follow starlingx.io/stalld.xxxx pattern
# Examples
# 'starlingx.io/stalld.boost_period'
# 'starlingx.io/stalld.boost_runtime'
# 'starlingx.io/stalld.boost_duration'
# 'starlingx.io/stalld.starving_threshold'
REGEX_STALLD_CUSTOM_LABEL = r"starlingx\.io\/stalld\.(\w+)"
CUSTOM_STALLD_LABEL_STRING = "starlingx.io/stalld."

View File

@ -20024,6 +20024,40 @@ class ConductorManager(service.PeriodicService):
"""
return cutils.get_secrets_info()
def configure_stalld(self, context, host_uuid):
""" Configure and restart the stalld daemon
:param context: admin context
:param ihost_uuid: host uuid
"""
host_uuid = host_uuid.strip()
try:
host = self.dbapi.ihost_get(host_uuid)
except exception.ServerNotFound:
LOG.info(f'Host not found {host_uuid}')
return None
hostname = host['hostname']
LOG.info(f'Attempting to configure stalld for host={hostname}')
personalities = [host['personality']]
host_uuids = [host['uuid']]
config_uuid = self._config_update_hosts(
context=context,
personalities=personalities,
host_uuids=host_uuids,
reboot=False)
config_dict = {
"personalities": personalities,
"host_uuids": host_uuids,
"classes": [
'platform::stalld::runtime'
],
}
self._config_apply_runtime_manifest(context,
config_uuid,
config_dict)
def device_image_state_sort_key(dev_img_state):
if dev_img_state.bitstream_type == dconstants.BITSTREAM_TYPE_ROOT_KEY:

View File

@ -16,7 +16,7 @@
# License for the specific language governing permissions and limitations
# under the License.
#
# Copyright (c) 2013-2024 Wind River Systems, Inc.
# Copyright (c) 2013-2025 Wind River Systems, Inc.
#
"""
@ -2309,3 +2309,13 @@ class ConductorAPI(sysinv.openstack.common.rpc.proxy.RpcProxy):
:param context: request context.
"""
return self.call(context, self.make_msg('get_all_k8s_certs'))
def configure_stalld(self, context, host_uuid):
"""Synchronously, have the conductor reconfigure stalld
for the specified host.
:param context: request context
:param host_uuid: the uuid of the host
"""
return self.call(context, self.make_msg('configure_stalld',
host_uuid=host_uuid))

View File

@ -8118,6 +8118,14 @@ class Connection(api.Connection):
query = query.filter_by(host_id=hostid)
return query.all()
@db_objects.objectify(objects.label)
def label_get_all_like(self, pattern, hostid=None):
query = model_query(models.Label, read_deleted="no")
if hostid:
query = query.filter_by(host_id=hostid)
query = query.filter(models.Label.label_key.like(pattern))
return query.all()
@db_objects.objectify(objects.label)
def label_update(self, uuid, values):
with _session_for_write() as session:

View File

@ -1,4 +1,4 @@
# Copyright (c) 2017-2024 Wind River Systems, Inc.
# Copyright (c) 2017-2025 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
@ -9,6 +9,7 @@ import ruamel.yaml as yaml
import re
import numpy as np
from contextlib import suppress
from oslo_log import log as logging
from oslo_serialization import base64
from sysinv.common import constants
@ -79,6 +80,7 @@ class PlatformPuppet(base.BasePuppet):
config.update(self._get_host_lldp_config(host))
config.update(self._get_ttys_dcd_config(host))
config.update(self._get_host_tuned_devices(host))
config.update(self._get_stalld_config(host))
return config
def get_host_config_upgrade(self, host):
@ -1156,3 +1158,102 @@ class PlatformPuppet(base.BasePuppet):
"platform::tty::params::active_device":
host.console.split(',')[0]
}
def _get_stalld_config(self, host):
LOG.debug(f"{host.hostname} _get_stalld_config()")
def _get_label_value(_label_key,
_supported_values,
_default_value):
label_value = _default_value
with suppress(exception.HostLabelNotFoundByKey):
label = self.dbapi.label_query(host.id, _label_key)
label_value = label.label_value.lower()
# unsupported value in database
if label_value not in _supported_values:
LOG.error(f"Unexpected {_label_key}='{label.label_value}' "
"using default values of "
f"'{_default_value}' instead")
label_value = _default_value
return label_value
stalld_enabled = _get_label_value(
constants.LABEL_STALLD,
constants.VALID_STALLD_VALUES,
constants.LABEL_VALUE_STALLD_DISABLED)
stalld_cpu_functions = _get_label_value(
constants.LABEL_STALLD_CPU_FUNCTIONS,
constants.VALID_STALLD_CPU_FUNCTION_VALUES,
constants.LABEL_VALUE_CPU_DEFAULT)
cpus = []
if stalld_cpu_functions == constants.LABEL_VALUE_CPU_ALL:
cpus = self._get_host_cpu_list(host,
threads=True)
elif stalld_cpu_functions == constants.LABEL_VALUE_CPU_APPLICATION_ISOLATED:
cpus = self._get_host_cpu_list(host,
constants.ISOLATED_FUNCTION,
threads=True)
else:
# constants.LABEL_VALUE_CPU_APPLICATION = 'application'
cpus_app = self._get_host_cpu_list(
host,
constants.APPLICATION_FUNCTION,
threads=True)
cpus_iso = self._get_host_cpu_list(
host,
constants.ISOLATED_FUNCTION,
threads=True)
cpus = cpus_app + cpus_iso
if not cpus:
LOG.error(f"{host.hostname} - {stalld_cpu_functions} "
"stalld cpu_list is empty")
LOG.error("stalld auto-disabled update cpu core assignment")
stalld_enabled = constants.LABEL_VALUE_STALLD_DISABLED
# generate cpu_list
cpus = sorted(cpus, key=lambda c: c.cpu)
cpu_set = set([c.cpu for c in cpus])
stalld_cpu_list = utils.format_range_set(cpu_set)
config = {
"platform::stalld::params::enable":
stalld_enabled == constants.LABEL_VALUE_STALLD_ENABLED,
"platform::stalld::params::cpu_list":
f"'{stalld_cpu_list}'"
}
# custom stalld labels
config.update(self._get_custom_stalld_config(host))
LOG.debug(f"{host.hostname} updating stalld config\n{config}")
return config
def _get_custom_stalld_config(self, host):
config = {}
stalld_label_list = self.dbapi.label_get_all_like(
pattern=f"{constants.CUSTOM_STALLD_LABEL_STRING}%",
hostid=host.id
)
for label in stalld_label_list:
label_key = label.label_key
label_value = label.label_value
# skip supported stalld label keys which are also case insensitive
if label_key.lower() in constants.SUPPORTED_STALLD_LABELS:
continue
custom_parameter = label_key.replace(
constants.CUSTOM_STALLD_LABEL_STRING,
''
)
config.update({
f"platform::stalld::params::{custom_parameter}":
f"'{label_value}'"
})
return config

View File

@ -1,9 +1,11 @@
# Copyright (c) 2019-2024 Wind River Systems, Inc.
# Copyright (c) 2019-2025 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
import itertools
import mock
import random
from six.moves import http_client
from six.moves.urllib.parse import urlencode
@ -12,6 +14,7 @@ from sysinv.db import api as dbapi
from sysinv.tests.api import base
from sysinv.api.controllers.v1 import label as policylabel
from sysinv.tests.db import utils as dbutils
from sysinv.tests.db import base as dbbase
import wsme
@ -39,10 +42,9 @@ def mock_get_system_enabled_k8s_plugins_return_none():
class LabelTestCase(base.FunctionalTest):
def setUp(self):
super(LabelTestCase, self).setUp()
self.dbapi = dbapi.get_instance()
self.system = dbutils.create_test_isystem()
def _get_path(self, host=None, params=None):
if host:
@ -79,8 +81,11 @@ class LabelTestCase(base.FunctionalTest):
class LabelAssignTestCase(LabelTestCase):
def setUp(self):
super(LabelAssignTestCase, self).setUp()
self.dbapi = dbapi.get_instance()
self.system = dbutils.create_test_isystem()
self.controller = dbutils.create_test_ihost(
id='1',
uuid=None,
@ -321,3 +326,411 @@ class LabelAssignTestCase(LabelTestCase):
response_data = self.get_host_labels(self.worker.uuid)
self.validate_labels(test_plugin_label, response_data)
class FakeConductorAPI(object):
def __init__(self):
self.update_kubernetes_label = mock.MagicMock()
self.update_grub_config = mock.MagicMock()
self.configure_power_manager = mock.MagicMock()
self.configure_stalld = mock.MagicMock()
class StalldLabelTestCase(LabelTestCase, dbbase.BaseHostTestCase):
def _create_host(self, personality, subfunction=None,
mgmt_mac=None, mgmt_ip=None,
admin=None,
invprovision=constants.PROVISIONED, **kw):
host = self._create_test_host(personality=personality,
subfunction=subfunction,
administrative=(admin or
constants.ADMIN_UNLOCKED),
invprovision=invprovision,
**kw)
return host
def _setup_context(self):
self.fake_conductor_api = FakeConductorAPI()
p = mock.patch('sysinv.conductor.rpcapiproxy.ConductorAPI')
self.mock_conductor_api = p.start()
self.mock_conductor_api.return_value = self.fake_conductor_api
self.addCleanup(p.stop)
def _create_standard_system(self):
self.controller = self._create_host(constants.CONTROLLER)
self.worker = self._create_host(constants.WORKER)
self._create_test_host_cpus(self.worker,
application=8)
self.storage = self._create_host(constants.STORAGE)
def _create_aio_system(self):
self.controller = self._create_host(constants.CONTROLLER,
subfunction=constants.WORKER)
self._create_test_host_cpus(self.controller,
platform=2,
application=6)
def setUp(self):
super(StalldLabelTestCase, self).setUp()
self._setup_context()
def test_stalld_enable_successful_on_aio_controller(self):
self._create_aio_system()
host_uuid = self.controller.uuid
input_data = {
constants.LABEL_STALLD: constants.LABEL_VALUE_STALLD_ENABLED
}
parameters = {'overwrite': True}
self.assign_labels(host_uuid, input_data, parameters)
response_data = self.get_host_labels(host_uuid)
self.validate_labels(input_data, response_data)
# Verify that the method configure_stalld() is called
self.fake_conductor_api.configure_stalld.assert_called_once()
def test_stalld_disable_successful_on_aio_controller(self):
self._create_aio_system()
host_uuid = self.controller.uuid
input_data = {
constants.LABEL_STALLD: constants.LABEL_VALUE_STALLD_DISABLED
}
parameters = {'overwrite': True}
self.assign_labels(host_uuid, input_data, parameters)
response_data = self.get_host_labels(host_uuid)
self.validate_labels(input_data, response_data)
# Verify that the method configure_stalld() is called
self.fake_conductor_api.configure_stalld.assert_called_once()
def test_stalld_enable_successful_on_worker_node(self):
self._create_standard_system()
host_uuid = self.worker.uuid
input_data = {
constants.LABEL_STALLD: constants.LABEL_VALUE_STALLD_ENABLED
}
parameters = {'overwrite': True}
self.assign_labels(host_uuid, input_data, parameters)
response_data = self.get_host_labels(host_uuid)
self.validate_labels(input_data, response_data)
# Verify that the method configure_stalld() is called
self.fake_conductor_api.configure_stalld.assert_called_once()
def test_stalld_disable_successful_on_worker_node(self):
self._create_standard_system()
host_uuid = self.worker.uuid
input_data = {
constants.LABEL_STALLD: constants.LABEL_VALUE_STALLD_DISABLED
}
parameters = {'overwrite': True}
self.assign_labels(host_uuid, input_data, parameters)
response_data = self.get_host_labels(host_uuid)
self.validate_labels(input_data, response_data)
# Verify that the method configure_stalld() is called
self.fake_conductor_api.configure_stalld.assert_called_once()
def test_stalld_enable_fails_on_standard_controller(self):
self._create_standard_system()
host_uuid = self.controller.uuid
input_data = {
constants.LABEL_STALLD: constants.LABEL_VALUE_STALLD_ENABLED
}
parameters = {'overwrite': True}
self.assign_labels_failure(host_uuid, input_data, parameters)
# Verify that the method configure_stalld() is not called
self.fake_conductor_api.configure_stalld.assert_not_called()
def test_stalld_enable_fails_on_storage_node(self):
self._create_standard_system()
host_uuid = self.storage.uuid
input_data = {
constants.LABEL_STALLD: constants.LABEL_VALUE_STALLD_ENABLED
}
parameters = {'overwrite': True}
self.assign_labels_failure(host_uuid, input_data, parameters)
# Verify that the method configure_stalld() is not called
self.fake_conductor_api.configure_stalld.assert_not_called()
def test_stalld_assign_application_cpus_successful(self):
"""Labels assigned together
"""
self._create_standard_system()
host_uuid = self.worker.uuid
input_data = {
constants.LABEL_STALLD: constants.LABEL_VALUE_STALLD_ENABLED,
constants.LABEL_STALLD_CPU_FUNCTIONS: constants.LABEL_VALUE_CPU_APPLICATION
}
parameters = {'overwrite': True}
self.assign_labels(host_uuid, input_data, parameters)
# Verify that the method configure_stalld() is called
self.fake_conductor_api.configure_stalld.assert_called_once()
def test_stalld_assign_all_cpus_successful(self):
"""Labels assigned together
"""
self._create_standard_system()
host_uuid = self.worker.uuid
input_data = {
constants.LABEL_STALLD: constants.LABEL_VALUE_STALLD_ENABLED,
constants.LABEL_STALLD_CPU_FUNCTIONS: constants.LABEL_VALUE_CPU_ALL
}
parameters = {'overwrite': True}
self.assign_labels(host_uuid, input_data, parameters)
# Verify that the method configure_stalld() is called
self.fake_conductor_api.configure_stalld.assert_called_once()
def test_stalld_assign_application_isolated_cpus_fails(self):
"""Fails because no cpus are assigned to application isolated function
on the worker node.
"""
self._create_standard_system()
host_uuid = self.worker.uuid
input_data = {
constants.LABEL_STALLD: constants.LABEL_VALUE_STALLD_ENABLED,
constants.LABEL_STALLD_CPU_FUNCTIONS: constants.LABEL_VALUE_CPU_APPLICATION_ISOLATED
}
parameters = {'overwrite': True}
self.assign_labels_failure(host_uuid, input_data, parameters)
# Verify that the method configure_stalld() is not called
self.fake_conductor_api.configure_stalld.assert_not_called()
def _generate_case_insensite_permutations(self,
label_values: list[str],
sample_size=5) -> list:
all_permutations = []
for label_value in label_values:
character_tuples = ((c.lower(), c.upper()) for c in label_value)
label_permutations = [
''.join(x) for x in itertools.product(*character_tuples)
]
# randomly sample 'n' permutation because the list could be very long
sample_size = min(sample_size, len(label_permutations))
all_permutations.extend(random.sample(label_permutations,
k=sample_size))
return all_permutations
def test_stalld_assign_case_insensitive(self):
"""Labels assigned together
"""
self._create_standard_system()
host_uuid = self.worker.uuid
label_values = self._generate_case_insensite_permutations([
'enabled',
'disabled'
])
parameters = {'overwrite': True}
for label_value in label_values:
input_data = {
constants.LABEL_STALLD: label_value
}
self.assign_labels(host_uuid, input_data, parameters)
# Verify that the method configure_stalld() is called
self.fake_conductor_api.configure_stalld.assert_called_once()
self.fake_conductor_api.configure_stalld.reset_mock()
def test_stalld_assign_cpu_function_case_insensitive(self):
"""Labels assigned together
"""
self._create_standard_system()
host_uuid = self.worker.uuid
label_values = self._generate_case_insensite_permutations([
'all',
'Application',
'Application-isolated'
])
parameters = {'overwrite': True}
for label_value in label_values:
input_data = {
constants.LABEL_STALLD_CPU_FUNCTIONS: label_value
}
self.assign_labels(host_uuid, input_data, parameters)
# Verify that the method configure_stalld() is called
self.fake_conductor_api.configure_stalld.assert_called_once()
self.fake_conductor_api.configure_stalld.reset_mock()
def test_stalld_enable_fails_if_assigned_app_iso_cpus_prior(self):
"""Fails because no cpus are assigned to application isolated function
on the worker node.
While stalld is disabled we can assign the isolated cpu functions
but if we try to enable stalld it will fail
"""
self._create_standard_system()
host_uuid = self.worker.uuid
input_data = {
constants.LABEL_STALLD: constants.LABEL_VALUE_STALLD_DISABLED,
constants.LABEL_STALLD_CPU_FUNCTIONS: constants.LABEL_VALUE_CPU_APPLICATION_ISOLATED
}
parameters = {'overwrite': True}
self.assign_labels(host_uuid, input_data, parameters)
# Verify that the method configure_stalld() is called
self.fake_conductor_api.configure_stalld.assert_called_once()
# after cpu functions are assigned attempt to enable stalld
self.fake_conductor_api.configure_stalld.reset_mock()
input_data = {
constants.LABEL_STALLD: constants.LABEL_VALUE_STALLD_ENABLED
}
parameters = {'overwrite': True}
self.assign_labels_failure(host_uuid, input_data, parameters)
# Verify that the method configure_stalld() is not called
self.fake_conductor_api.configure_stalld.assert_not_called()
def test_stalld_assign_cpus_before_enable_successful(self):
"""Labels assigned in sequence
1. starlingx.io/stalld_cpu_functions=application
2. starlingx.io/stalld_cpu_functions=all
3. starlingx.io/stalld=enabled
"""
self._create_standard_system()
host_uuid = self.worker.uuid
input_data = {
constants.LABEL_STALLD_CPU_FUNCTIONS: constants.LABEL_VALUE_CPU_APPLICATION
}
parameters = {'overwrite': True}
self.assign_labels(host_uuid, input_data, parameters)
# Verify that the method configure_stalld() is called
self.fake_conductor_api.configure_stalld.assert_called_once()
self.fake_conductor_api.configure_stalld.reset_mock()
input_data = {
constants.LABEL_STALLD_CPU_FUNCTIONS: constants.LABEL_VALUE_CPU_ALL
}
parameters = {'overwrite': True}
self.assign_labels(host_uuid, input_data, parameters)
# Verify that the method configure_stalld() is called
self.fake_conductor_api.configure_stalld.assert_called_once()
self.fake_conductor_api.configure_stalld.reset_mock()
input_data = {
constants.LABEL_STALLD: constants.LABEL_VALUE_STALLD_ENABLED
}
parameters = {'overwrite': True}
self.assign_labels(host_uuid, input_data, parameters)
# Verify that the method configure_stalld() is called
self.fake_conductor_api.configure_stalld.assert_called_once()
def test_stalld_assign_different_cpu_functions(self):
"""Labels assigned in sequence
1. starlingx.io/stalld=enabled
2. starlingx.io/stalld_cpu_functions=all
3. starlingx.io/stalld_cpu_functions=application
4. starlingx.io/stalld_cpu_functions=application-isolated <- fails
5. assign application-isolated function to 1 cpu of the worker node
6. starlingx.io/stalld_cpu_functions=application-isolated <- success
"""
self._create_standard_system()
host_uuid = self.worker.uuid
input_data = {
constants.LABEL_STALLD: constants.LABEL_VALUE_STALLD_ENABLED
}
parameters = {'overwrite': True}
self.assign_labels(host_uuid, input_data, parameters)
# Verify that the method configure_stalld() is called
self.fake_conductor_api.configure_stalld.assert_called_once()
self.fake_conductor_api.configure_stalld.reset_mock()
input_data = {
constants.LABEL_STALLD_CPU_FUNCTIONS: constants.LABEL_VALUE_CPU_ALL
}
parameters = {'overwrite': True}
self.assign_labels(host_uuid, input_data, parameters)
# Verify that the method configure_stalld() is called
self.fake_conductor_api.configure_stalld.assert_called_once()
self.fake_conductor_api.configure_stalld.reset_mock()
input_data = {
constants.LABEL_STALLD_CPU_FUNCTIONS: constants.LABEL_VALUE_CPU_APPLICATION
}
parameters = {'overwrite': True}
self.assign_labels(host_uuid, input_data, parameters)
# Verify that the method configure_stalld() is called
self.fake_conductor_api.configure_stalld.assert_called_once()
self.fake_conductor_api.configure_stalld.reset_mock()
input_data = {
constants.LABEL_STALLD_CPU_FUNCTIONS: constants.LABEL_VALUE_CPU_APPLICATION_ISOLATED
}
parameters = {'overwrite': True}
self.assign_labels_failure(host_uuid, input_data, parameters)
# Verify that the method configure_stalld() is called
self.fake_conductor_api.configure_stalld.assert_not_called()
self.fake_conductor_api.configure_stalld.reset_mock()
# Change the last cpu to application-isolated
last_cpu = self.dbapi.icpu_get_by_ihost(host_uuid)[-1]
values = {"allocated_function": constants.ISOLATED_FUNCTION}
self.dbapi.icpu_update(last_cpu.uuid, values)
# try again
self.assign_labels(host_uuid, input_data, parameters)
# Verify that the method configure_stalld() is called
self.fake_conductor_api.configure_stalld.assert_called_once()
def test_stalld_assign_custom_label_to_worker_successful(self):
"""Custom stalld label on worker node
1. starlingx.io/stalld.custom_label=custom_value
"""
self._create_standard_system()
host_uuid = self.worker.uuid
custom_stalld_label = f"{constants.LABEL_STALLD}.customlabel"
input_data = {
custom_stalld_label: "custom_value"
}
parameters = {'overwrite': True}
self.assign_labels(host_uuid, input_data, parameters)
# Verify that the method configure_stalld() is called
self.fake_conductor_api.configure_stalld.assert_called_once()
def test_stalld_assign_custom_label_to_storage_fails(self):
"""Custom stalld label on worker node
1. starlingx.io/stalld.custom_label=custom_value
"""
self._create_standard_system()
host_uuid = self.storage.uuid
custom_stalld_label = f"{constants.LABEL_STALLD}.customlabel"
input_data = {
custom_stalld_label: "custom_value"
}
parameters = {'overwrite': True}
self.assign_labels_failure(host_uuid, input_data, parameters)
# Verify that the method configure_stalld() is not called
self.fake_conductor_api.configure_stalld.assert_not_called()
def test_stalld_assign_custom_label_invalid_format(self):
"""Custom stalld label on worker node
1. starlingx.io/stalld_custom_label=custom_value
should be a '.' not an '_' character
"""
self._create_standard_system()
host_uuid = self.worker.uuid
custom_stalld_label = f"{constants.LABEL_STALLD}_customlabel"
input_data = {
custom_stalld_label: "custom_value"
}
parameters = {'overwrite': True}
self.assign_labels_failure(host_uuid, input_data, parameters)
# Verify that the method configure_stalld() is not called
self.fake_conductor_api.configure_stalld.assert_not_called()

View File

@ -5849,6 +5849,51 @@ class ManagerTestCase(base.DbTestCase):
# update only 1 time
mock_update_cached_app_bundles_set.assert_called_once()
@mock.patch('sysinv.conductor.manager.'
'ConductorManager._config_apply_runtime_manifest')
@mock.patch('sysinv.conductor.manager.'
'ConductorManager._config_update_hosts')
def test_configure_stalld(self,
mock_config_update_hosts,
mock_config_apply_runtime_manifest):
self._create_test_ihosts()
hostname = 'compute-0'
host = self.service.get_ihost_by_hostname(self.context, hostname)
host_uuid = host['uuid']
personalities = [host['personality']]
host_uuids = [host_uuid]
config_dict = {
"personalities": personalities,
"host_uuids": host_uuids,
"classes": [
'platform::stalld::runtime'
],
}
config_uuid = '1234'
mock_config_update_hosts.return_value = config_uuid
self.service.configure_stalld(context=self.context,
host_uuid=host_uuid)
mock_config_update_hosts.assert_called_once()
mock_config_apply_runtime_manifest.assert_called_once_with(
mock.ANY,
config_uuid,
config_dict)
@mock.patch('sysinv.conductor.manager.'
'ConductorManager._config_apply_runtime_manifest')
@mock.patch('sysinv.conductor.manager.'
'ConductorManager._config_update_hosts')
def test_configure_stalld_host_not_found(self,
mock_config_update_hosts,
mock_config_apply_runtime_manifest):
host_uuid = str(uuid.uuid4())
self.service.configure_stalld(context=self.context,
host_uuid=host_uuid)
mock_config_update_hosts.assert_not_called()
mock_config_apply_runtime_manifest.assert_not_called()
class ManagerTestCaseInternal(base.BaseHostTestCase):
def setUp(self):

View File

@ -116,3 +116,8 @@ class RPCAPITestCase(base.DbTestCase):
'cast',
ihost_uuid=self.fake_ihost['uuid'],
kernel_running=constants.KERNEL_LOWLATENCY)
def test_configure_stalld(self):
self._test_rpcapi('configure_stalld',
'call',
host_uuid=self.fake_ihost['uuid'])

View File

@ -1,10 +1,11 @@
# Copyright (c) 2019-2024 Wind River Systems, Inc.
# Copyright (c) 2019-2025 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
import mock
from sysinv.tests.db import base as dbbase
from sysinv.tests.db import utils as dbutils
from sysinv.tests.puppet import base
from sysinv.common import constants
@ -193,3 +194,334 @@ class PlatformTestCaseKubernetesReservedMemory(base.PuppetTestCaseMixin,
'platform::kubernetes::params::k8s_reserved_memory': expected_k8s_reserved_memory}
)
self.assertEqual(k8s_reserved_memory_config, config)
class PlatformTestCaseStalldConfig(base.PuppetTestCaseMixin,
dbbase.BaseHostTestCase):
def _create_test_host_cpus(self, host,
platform=0, application=0, isolated=0,
threads=1):
counts = [platform, application, isolated]
functions = [constants.PLATFORM_FUNCTION,
constants.APPLICATION_FUNCTION,
constants.ISOLATED_FUNCTION]
nodes = self.dbapi.inode_get_by_ihost(host.id)
for node in nodes:
cpu = 0
for count, function in zip(counts, functions):
for _ in range(0, count):
for thread in range(0, threads):
self.dbapi.icpu_create(host.id,
dbutils.get_test_icpu(
forinodeid=node.id,
cpu=cpu, thread=thread,
allocated_function=function))
cpu = cpu + 1
def _configure_cpus(self, platform=0, application=0, isolated=0):
self._create_test_host_cpus(self.host,
platform=platform,
application=application,
isolated=isolated)
self._create_test_host_addresses(self.host.hostname)
def _create_test_host_cpus_using_spec(self,
host,
cpu_assignment_spec: dict):
node = self.dbapi.inode_get_by_ihost(host.id)[0]
for function, _d in cpu_assignment_spec.items():
for thread, cpu_ids in _d.items():
for cpu_id in cpu_ids:
icpu = dbutils.get_test_icpu(forinodeid=node.id,
cpu=cpu_id,
thread=thread,
allocated_function=function)
self.dbapi.icpu_create(host.id, icpu)
def _configure_cpus_using_assignment_spec(self, cpu_assignment_spec):
self._create_test_host_cpus_using_spec(self.host, cpu_assignment_spec)
self._create_test_host_addresses(self.host.hostname)
def _create_host_labels_in_db(self, labels):
for label_str in labels:
k, v = label_str.split('=')
self.dbapi.label_create(self.host.id,
{'host_id': self.host.id,
'label_key': k,
'label_value': v})
def setUp(self):
super(PlatformTestCaseStalldConfig, self).setUp()
self.host = self._create_test_host(constants.WORKER)
def test_get_stalld_config_defaults(self):
""" stalld disabled with application default
0-1 platform cpus
2-7 application cpus
8-9 isolated cpus
"""
self._configure_cpus(platform=2, application=6, isolated=2)
stalld_cpu_list = "2-9"
self.operator.update_host_config(self.host)
self.assertConfigParameters(self.mock_write_config, {
"platform::stalld::params::enable":
False,
"platform::stalld::params::cpu_list":
f"'{stalld_cpu_list}'"
})
def test_get_stalld_config_enabled_cpus_default(self):
""" stalld enabled with cpu default=application
0-1 platform cpus
2-7 application cpus
8-9 isolated cpus
"""
self._configure_cpus(platform=2, application=6, isolated=2)
labels = [
# starlingx.io/stalld=enabled
f"{constants.LABEL_STALLD}={constants.LABEL_VALUE_STALLD_ENABLED}"
]
self._create_host_labels_in_db(labels)
stalld_cpu_list = "2-9"
self.operator.update_host_config(self.host)
self.assertConfigParameters(self.mock_write_config, {
"platform::stalld::params::enable":
True,
"platform::stalld::params::cpu_list":
f"'{stalld_cpu_list}'"
})
def test_get_stalld_config_enabled_cpus_all(self):
""" stalld enabled with all cpus
0-1 platform cpus
2-7 application cpus
8-9 isolated cpus
"""
self._configure_cpus(platform=2, application=6, isolated=2)
labels = [
# starlingx.io/stalld=enabled
f"{constants.LABEL_STALLD}={constants.LABEL_VALUE_STALLD_ENABLED}",
# starlingx.io/stalld_cpu_functions=all
f"{constants.LABEL_STALLD_CPU_FUNCTIONS}={constants.LABEL_VALUE_CPU_ALL}"
]
self._create_host_labels_in_db(labels)
stalld_cpu_list = "0-9"
self.operator.update_host_config(self.host)
self.assertConfigParameters(self.mock_write_config, {
"platform::stalld::params::enable":
True,
"platform::stalld::params::cpu_list":
f"'{stalld_cpu_list}'"
})
def test_get_stalld_config_enabled_cpus_application(self):
""" stalld enabled with application cpus
0-1 platform cpus
2-7 application cpus
8-9 isolated cpus
"""
self._configure_cpus(platform=2, application=6, isolated=2)
labels = [
# starlingx.io/stalld=enabled
f"{constants.LABEL_STALLD}={constants.LABEL_VALUE_STALLD_ENABLED}",
# starlingx.io/stalld_cpu_functions=application
f"{constants.LABEL_STALLD_CPU_FUNCTIONS}={constants.LABEL_VALUE_CPU_APPLICATION}"
]
self._create_host_labels_in_db(labels)
stalld_cpu_list = "2-9"
self.operator.update_host_config(self.host)
self.assertConfigParameters(self.mock_write_config, {
"platform::stalld::params::enable":
True,
"platform::stalld::params::cpu_list":
f"'{stalld_cpu_list}'"
})
def test_get_stalld_config_enabled_cpus_isolated(self):
""" stalld enabled with application-isolated cpus
0-1 platform cpus
2-7 application cpus
8-9 isolated cpus
"""
self._configure_cpus(platform=2, application=6, isolated=2)
labels = [
# starlingx.io/stalld=enabled
f"{constants.LABEL_STALLD}={constants.LABEL_VALUE_STALLD_ENABLED}",
# starlingx.io/stalld_cpu_functions=application-isolated
f"{constants.LABEL_STALLD_CPU_FUNCTIONS}={constants.LABEL_VALUE_CPU_APPLICATION_ISOLATED}"
]
self._create_host_labels_in_db(labels)
stalld_cpu_list = "8-9"
self.operator.update_host_config(self.host)
self.assertConfigParameters(self.mock_write_config, {
"platform::stalld::params::enable":
True,
"platform::stalld::params::cpu_list":
f"'{stalld_cpu_list}'"
})
def test_get_stalld_config_case_insensitive(self):
""" stalld enabled with application-isolated cpus
0-1 platform cpus
2-7 application cpus
8-9 isolated cpus
"""
self._configure_cpus(platform=2, application=6, isolated=2)
labels = [
# starlingx.io/stalld=enabled
f"{constants.LABEL_STALLD}=EnablED",
# starlingx.io/stalld_cpu_functions=application-isolated
f"{constants.LABEL_STALLD_CPU_FUNCTIONS}=APPlicaTion-iSOLAted"
]
self._create_host_labels_in_db(labels)
stalld_cpu_list = "8-9"
self.operator.update_host_config(self.host)
self.assertConfigParameters(self.mock_write_config, {
"platform::stalld::params::enable":
True,
"platform::stalld::params::cpu_list":
f"'{stalld_cpu_list}'"
})
def test_get_stalld_config_enabled_cpus_isolated_not_configured(self):
""" stalld enabled with application-isolated cpus
but cpu list is empty so stalld will be disabled in config
0-1 platform cpus
2-9 application cpus
n/a isolated cpus
"""
self._configure_cpus(platform=2, application=8, isolated=0)
labels = [
# starlingx.io/stalld=enabled
f"{constants.LABEL_STALLD}={constants.LABEL_VALUE_STALLD_ENABLED}",
# starlingx.io/stalld_cpu_functions=application-isolated
f"{constants.LABEL_STALLD_CPU_FUNCTIONS}={constants.LABEL_VALUE_CPU_APPLICATION_ISOLATED}"
]
self._create_host_labels_in_db(labels)
stalld_cpu_list = ""
self.operator.update_host_config(self.host)
self.assertConfigParameters(self.mock_write_config, {
"platform::stalld::params::enable":
False,
"platform::stalld::params::cpu_list":
f"'{stalld_cpu_list}'"
})
def test_get_stalld_config_bad_data_in_db(self):
""" stalld enabled with bad value
stalld defaults to disabled
0-1 platform cpus
2-7 application cpus
8-9 isolated cpus
"""
self._configure_cpus(platform=2, application=6, isolated=2)
labels = [
# starlingx.io/stalld=%#@#%#@
f"{constants.LABEL_STALLD}=%#@#%#@",
]
self._create_host_labels_in_db(labels)
stalld_cpu_list = "2-9"
self.operator.update_host_config(self.host)
self.assertConfigParameters(self.mock_write_config, {
"platform::stalld::params::enable":
False,
"platform::stalld::params::cpu_list":
f"'{stalld_cpu_list}'"
})
def test_get_stalld_config_cpus_bad_data_in_db(self):
""" stalld enabled with bad cpu function value
stalld cpu function defaults to application
0-1 platform cpus
2-7 application cpus
8-9 isolated cpus
"""
self._configure_cpus(platform=2, application=6, isolated=2)
labels = [
# starlingx.io/stalld=enabled
f"{constants.LABEL_STALLD}={constants.LABEL_VALUE_STALLD_ENABLED}",
# starlingx.io/stalld_cpu_functions=%#@#%#@
f"{constants.LABEL_STALLD_CPU_FUNCTIONS}=%#@#%#@"
]
self._create_host_labels_in_db(labels)
stalld_cpu_list = "2-9"
self.operator.update_host_config(self.host)
self.assertConfigParameters(self.mock_write_config, {
"platform::stalld::params::enable":
True,
"platform::stalld::params::cpu_list":
f"'{stalld_cpu_list}'"
})
def test_get_stalld_config_enabled_cpus_application_with_threads(self):
""" stalld enabled with application cpus (with 2 threads)
and discontinous cpu ids
platform: 0-1 ... thread 0
10-11 ... thread 1
application: 2-7 ... thread 0
12-17 ... thread 1
isolated: 8-9 ... thread 0
18-19 ... thread 1
"""
_THREAD_0, _THREAD_1 = 0, 1
cpu_assignment_spec = {
constants.PLATFORM_FUNCTION: {
_THREAD_0: [0, 1],
_THREAD_1: [10, 11]
},
constants.APPLICATION_FUNCTION: {
_THREAD_0: [2, 3, 4, 5, 6, 7],
_THREAD_1: [12, 13, 14, 15, 16, 17]
},
constants.ISOLATED_FUNCTION: {
_THREAD_0: [8, 9],
_THREAD_1: [18, 19]
}
}
# Add application CPUs
self._configure_cpus_using_assignment_spec(cpu_assignment_spec)
labels = [
f"{constants.LABEL_STALLD}={constants.LABEL_VALUE_STALLD_ENABLED}",
f"{constants.LABEL_STALLD_CPU_FUNCTIONS}={constants.LABEL_VALUE_CPU_APPLICATION_ISOLATED}"
]
self._create_host_labels_in_db(labels)
stalld_cpu_list = "8-9,18-19"
self.operator.update_host_config(self.host)
self.assertConfigParameters(self.mock_write_config, {
"platform::stalld::params::enable": True,
"platform::stalld::params::cpu_list": f"'{stalld_cpu_list}'"
})
def test_get_stalld_config_enabled_cpus_all_with_custom_parameters(self):
""" stalld enabled with all cpus
0-1 platform cpus
2-7 application cpus
8-9 isolated cpus
"""
self._configure_cpus(platform=2, application=6, isolated=2)
custom_parameter1, value1 = "string_parameter", "string_value1"
custom_parameter2, value2 = "numeric_parameter", 1792992
labels = [
# starlingx.io/stalld=enabled
f"{constants.LABEL_STALLD}={constants.LABEL_VALUE_STALLD_ENABLED}",
# starlingx.io/stalld_cpu_functions=all
f"{constants.LABEL_STALLD_CPU_FUNCTIONS}={constants.LABEL_VALUE_CPU_ALL}",
f"{constants.CUSTOM_STALLD_LABEL_STRING}{custom_parameter1}={value1}",
f"{constants.CUSTOM_STALLD_LABEL_STRING}{custom_parameter2}={value2}"
]
self._create_host_labels_in_db(labels)
stalld_cpu_list = "0-9"
self.operator.update_host_config(self.host)
self.assertConfigParameters(self.mock_write_config, {
"platform::stalld::params::enable":
True,
"platform::stalld::params::cpu_list":
f"'{stalld_cpu_list}'",
f"platform::stalld::params::{custom_parameter1}":
f"'{value1}'",
f"platform::stalld::params::{custom_parameter2}":
f"'{value2}'"
})