diff --git a/sysinv/cgts-client/cgts-client/cgtsclient/common/constants.py b/sysinv/cgts-client/cgts-client/cgtsclient/common/constants.py index 2d0acfca8a..c62dfee4d7 100755 --- a/sysinv/cgts-client/cgts-client/cgtsclient/common/constants.py +++ b/sysinv/cgts-client/cgts-client/cgtsclient/common/constants.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2013-2023 Wind River Systems, Inc. +# Copyright (c) 2013-2023, 2025 Wind River Systems, Inc. # # SPDX-License-Identifier: Apache-2.0 # @@ -113,6 +113,13 @@ SB_SUPPORTED_NETWORKS = { SB_TYPE_CEPH: [NETWORK_TYPE_MGMT, NETWORK_TYPE_CLUSTER_HOST] } +# PTP definitions +PTP_INSTANCE_TYPE_MONITORING = "monitoring" +PTP_INSTANCE_TYPE_MONITORING_SUPPORTED_PARAMETERS = [ + "devices", "satellite_count", "signal_quality_db", + "cmdline_opts" +] + EXPIRED = "--expired" SOON_TO_EXPIRY = "--soon_to_expiry" VALIDITY = "Validity" diff --git a/sysinv/cgts-client/cgts-client/cgtsclient/v1/ptp_instance_shell.py b/sysinv/cgts-client/cgts-client/cgtsclient/v1/ptp_instance_shell.py index 4359b2eb2d..f90dac7fd1 100644 --- a/sysinv/cgts-client/cgts-client/cgtsclient/v1/ptp_instance_shell.py +++ b/sysinv/cgts-client/cgts-client/cgtsclient/v1/ptp_instance_shell.py @@ -1,11 +1,12 @@ ######################################################################## # -# Copyright (c) 2021-2023 Wind River Systems, Inc. +# Copyright (c) 2021-2023, 2025 Wind River Systems, Inc. # # SPDX-License-Identifier: Apache-2.0 # ######################################################################## +from cgtsclient.common import constants from cgtsclient.common import utils from cgtsclient import exc from cgtsclient.v1 import ihost as ihost_utils @@ -45,7 +46,10 @@ def do_ptp_instance_list(cc, args): help="Name of PTP instance [REQUIRED]") @utils.arg('service', metavar='', - choices=['ptp4l', 'phc2sys', 'ts2phc', 'clock', 'synce4l'], + choices=[ + 'ptp4l', 'phc2sys', 'ts2phc', 'clock', 'synce4l', + constants.PTP_INSTANCE_TYPE_MONITORING + ], help="Service type [REQUIRED]") def do_ptp_instance_add(cc, args): """Add a PTP instance.""" @@ -81,6 +85,20 @@ def _ptp_instance_parameter_op(cc, op, instance, parameters): if len(parameters) == 0: raise exc.CommandError('Missing PTP parameter') ptp_instance = ptp_instance_utils._find_ptp_instance(cc, instance) + + # check for supported parameters in case of monitoring type + if (ptp_instance.service == constants.PTP_INSTANCE_TYPE_MONITORING and op == "add"): + for param_keypair in parameters: + if param_keypair.find("=") < 0: + raise exc.CommandError(f"Bad PTP parameter keypair: {param_keypair}") + (param_name, param_value) = param_keypair.split("=", 1) + + if param_name not in constants.PTP_INSTANCE_TYPE_MONITORING_SUPPORTED_PARAMETERS: + raise exc.CommandError( + f"Parameter {param_name} is not supported. Supported parameters:" + f"{constants.PTP_INSTANCE_TYPE_MONITORING_SUPPORTED_PARAMETERS}" + ) + patch = [] for parameter in parameters: patch.append({'op': op, diff --git a/sysinv/cgts-client/cgts-client/cgtsclient/v1/ptp_interface_shell.py b/sysinv/cgts-client/cgts-client/cgtsclient/v1/ptp_interface_shell.py index 88a38ea821..e9547c41c0 100644 --- a/sysinv/cgts-client/cgts-client/cgtsclient/v1/ptp_interface_shell.py +++ b/sysinv/cgts-client/cgts-client/cgtsclient/v1/ptp_interface_shell.py @@ -1,11 +1,12 @@ ######################################################################## # -# Copyright (c) 2021 Wind River Systems, Inc. +# Copyright (c) 2021, 2025 Wind River Systems, Inc. # # SPDX-License-Identifier: Apache-2.0 # ######################################################################## +from cgtsclient.common import constants from cgtsclient.common import utils from cgtsclient import exc from cgtsclient.v1 import ihost as ihost_utils @@ -60,6 +61,13 @@ def do_ptp_interface_add(cc, args): # Check the PTP instance exists ptp_instance = ptp_instance_utils._find_ptp_instance( cc, args.ptpinstancenameorid) + + # Do not allow for monitoring service type. + if ptp_instance.service == constants.PTP_INSTANCE_TYPE_MONITORING: + raise exc.CommandError( + "PTP instance of monitoring type does not support interface" + ) + data.update({'ptp_instance_uuid': ptp_instance.uuid}) ptp_interface = cc.ptp_interface.create(**data) diff --git a/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/host.py b/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/host.py index 053bf3084f..45506266c1 100644 --- a/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/host.py +++ b/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/host.py @@ -2036,6 +2036,15 @@ class HostController(rest.RestController): constants.NETWORK_TYPE_MGMT, True) return address.address + def _exists_monitoring_instance_on_host(self, host_uuid): + ptp_instances = pecan.request.dbapi.ptp_instances_get_list(host_uuid) + + for instance in ptp_instances: + if instance["service"] == constants.PTP_INSTANCE_TYPE_MONITORING: + return True + + return False + def _patch(self, uuid, patch): log_start = cutils.timestamped("ihost_patch_start") @@ -2048,12 +2057,22 @@ class HostController(rest.RestController): ptp_instance_id = p.get('value') try: # Check PTP instance exists - pecan.request.dbapi.ptp_instance_get(ptp_instance_id) + ptp_instance = pecan.request.dbapi.ptp_instance_get(ptp_instance_id) except exception.PtpInstanceNotFound: raise wsme.exc.ClientSideError(_("No PTP instance object")) values = {'host_id': ihost_obj.id, 'ptp_instance_id': ptp_instance_id} if p.get('op') == constants.PTP_PATCH_OPERATION_ADD: + # Check constraint: single monitoring ptp instance on host + if ptp_instance[ + "service" + ] == constants.PTP_INSTANCE_TYPE_MONITORING and self._exists_monitoring_instance_on_host( + uuid + ): + raise wsme.exc.ClientSideError( + _("Monitoring ptp instance already exists on host") + ) + pecan.request.dbapi.ptp_instance_assign(values) else: pecan.request.dbapi.ptp_instance_remove(values) diff --git a/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/ptp_instance.py b/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/ptp_instance.py index d5abb973e3..59b9f5b4a7 100644 --- a/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/ptp_instance.py +++ b/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/ptp_instance.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2021-2023 Wind River Systems, Inc. +# Copyright (c) 2021-2023, 2025 Wind River Systems, Inc. # # SPDX-License-Identifier: Apache-2.0 # @@ -74,7 +74,8 @@ class PtpInstance(base.APIBase): constants.PTP_INSTANCE_TYPE_PHC2SYS, constants.PTP_INSTANCE_TYPE_TS2PHC, constants.PTP_INSTANCE_TYPE_CLOCK, - constants.PTP_INSTANCE_TYPE_SYNCE4L) + constants.PTP_INSTANCE_TYPE_SYNCE4L, + constants.PTP_INSTANCE_TYPE_MONITORING) "Type of service of the PTP instance" hostnames = types.MultiType([list]) diff --git a/sysinv/sysinv/sysinv/sysinv/common/constants.py b/sysinv/sysinv/sysinv/sysinv/common/constants.py index 3f128ab3ec..e075bd7670 100644 --- a/sysinv/sysinv/sysinv/sysinv/common/constants.py +++ b/sysinv/sysinv/sysinv/sysinv/common/constants.py @@ -2403,6 +2403,10 @@ PTP_SYNCE_EEC_INVALID_VALUE = '0' PTP_SYNCE_TX_HEARTBEAT_MSEC = '1000' PTP_SYNCE_RX_HEARTBEAT_MSEC = '500' +# PTP instance monitoring default parameters +PTP_MONITORING_SATELLITE_COUNT = "5" +PTP_MONITORING_SIGNAL_QUALITY_DB_VALUE = "30" + # PTP pmc values PTP_PMC_CLOCK_CLASS = '248' PTP_PMC_CLOCK_ACCURACY = '0xfe' @@ -2422,6 +2426,7 @@ PTP_INSTANCE_TYPE_PHC2SYS = 'phc2sys' PTP_INSTANCE_TYPE_TS2PHC = 'ts2phc' PTP_INSTANCE_TYPE_CLOCK = 'clock' PTP_INSTANCE_TYPE_SYNCE4L = 'synce4l' +PTP_INSTANCE_TYPE_MONITORING = "monitoring" # PTP instances created during migration PTP_INSTANCE_LEGACY_PTP4L = 'ptp4l-legacy' diff --git a/sysinv/sysinv/sysinv/sysinv/puppet/networking.py b/sysinv/sysinv/sysinv/sysinv/puppet/networking.py index c4b0cec022..5929f19c23 100644 --- a/sysinv/sysinv/sysinv/sysinv/puppet/networking.py +++ b/sysinv/sysinv/sysinv/sysinv/puppet/networking.py @@ -323,8 +323,38 @@ class NetworkingPuppet(base.BasePuppet): return config - def _set_ptp_instance_global_parameters(self, ptp_instances, ptp_parameters_instance): + def _set_ptp_instance_monitoring_global_parameters( + self, instance, ptp_parameters_instance + ): + default_global_parameters = { + "satellite_count": constants.PTP_MONITORING_SATELLITE_COUNT, + "signal_quality_db": constants.PTP_MONITORING_SIGNAL_QUALITY_DB_VALUE, + } + default_cmdline_opts = "" + # Add default global parameters the instance + instance["global_parameters"] = default_global_parameters + instance["cmdline_opts"] = default_cmdline_opts + + for global_param in ptp_parameters_instance: + # Add the supplied instance parameters to global_parameters + if instance["uuid"] in global_param["owners"]: + instance["global_parameters"][global_param["name"]] = global_param[ + "value" + ] + if "cmdline_opts" in instance["global_parameters"]: + cmdline = instance["global_parameters"].pop("cmdline_opts") + quotes = {"'", "\\'", '"', '\\"'} + for quote in quotes: + cmdline = cmdline.strip(quote) + instance["cmdline_opts"] = cmdline + + allowed_instance_fields = ["global_parameters", "cmdline_opts"] + monitoring_config = {r: instance[r] for r in allowed_instance_fields} + + return monitoring_config + + def _set_ptp_instance_global_parameters(self, ptp_instances, ptp_parameters_instance): default_global_parameters = { # Default ptp4l parameters were determined during the original integration of PTP. # These defaults maintain the same PTP behaviour as single instance implementation. @@ -713,12 +743,20 @@ class NetworkingPuppet(base.BasePuppet): nic_clock_config = {} nic_clock_enabled = False ptp_instance_configs = [] + monitoring_instance_configs = [] for index, instance in enumerate(ptp_instances): if ptp_instances[index]['service'] == constants.PTP_INSTANCE_TYPE_CLOCK: clock_instance = ptp_instances[index] nic_clocks[instance['name']] = clock_instance.as_dict() nic_clocks[instance['name']]['interfaces'] = [] + elif ( + ptp_instances[index]["service"] + == constants.PTP_INSTANCE_TYPE_MONITORING + ): + ptp_instances[index][instance["name"]] = instance.as_dict() + ptp_instances[index][instance["name"]]["interfaces"] = [] + monitoring_instance_configs.append(ptp_instances[index]) else: ptp_instances[index][instance['name']] = instance.as_dict() ptp_instances[index][instance['name']]['interfaces'] = [] @@ -751,14 +789,34 @@ class NetworkingPuppet(base.BasePuppet): else: ptp_config = {} - return {'platform::ptpinstance::config': ptp_config, - 'platform::ptpinstance::enabled': ptpinstance_enabled, - 'platform::ptpinstance::nic_clock::nic_clock_config': nic_clock_config, - 'platform::ptpinstance::nic_clock::nic_clock_enabled': nic_clock_enabled} + # Generate the monitoring config + monitoring_enabled = False + monitoring_config = {} + len_monitoring_instance_configs = len(monitoring_instance_configs) + + if ptpinstance_enabled and len_monitoring_instance_configs > 0: + # Only single monitoring instance per host is allowed. + if len_monitoring_instance_configs == 1: + monitoring_enabled = True + monitoring_config = self._set_ptp_instance_monitoring_global_parameters( + monitoring_instance_configs[0], ptp_parameters_instance + ) + else: + LOG.warning( + f"PTP monitoring instances are {len_monitoring_instance_configs > 1} on host id {host.id}." + ) + + return { + 'platform::ptpinstance::config': ptp_config, + 'platform::ptpinstance::enabled': ptpinstance_enabled, + 'platform::ptpinstance::monitoring::monitoring_config': monitoring_config, + 'platform::ptpinstance::monitoring::monitoring_enabled': monitoring_enabled, + 'platform::ptpinstance::nic_clock::nic_clock_config': nic_clock_config, + 'platform::ptpinstance::nic_clock::nic_clock_enabled': nic_clock_enabled, + } def _get_interface_config(self, networktype): config = {} - network_interface = interface.find_interface_by_type( self.context, networktype) diff --git a/sysinv/sysinv/sysinv/sysinv/tests/api/test_ptp_instance.py b/sysinv/sysinv/sysinv/sysinv/tests/api/test_ptp_instance.py index af73019564..5f104b80e9 100644 --- a/sysinv/sysinv/sysinv/sysinv/tests/api/test_ptp_instance.py +++ b/sysinv/sysinv/sysinv/sysinv/tests/api/test_ptp_instance.py @@ -1,4 +1,4 @@ -# Copyright (c) 2021-2022 Wind River Systems, Inc. +# Copyright (c) 2021-2022, 2025 Wind River Systems, Inc. # # SPDX-License-Identifier: Apache-2.0 # @@ -91,6 +91,11 @@ class TestCreatePtpInstance(BasePtpInstanceTestCase): status_code=http_client.CONFLICT, error_message=error_message) + def test_create_ptp_instance_monitoring_ok(self): + self._create_ptp_instance_success( + "fake-instance-monitoring", constants.PTP_INSTANCE_TYPE_MONITORING + ) + class TestHostPtpInstance(BasePtpInstanceTestCase): def setUp(self): @@ -140,6 +145,57 @@ class TestHostPtpInstance(BasePtpInstanceTestCase): self.assertEqual(response.content_type, 'application/json') self.assertEqual(response.status_code, http_client.OK) + def _assign_host_ptp_instance_monitoring_success(self): + ptp_instance = dbutils.create_test_ptp_instance( + name="test-instance", service=constants.PTP_INSTANCE_TYPE_MONITORING + ) + ptp_instance_id = ptp_instance["id"] + response = self.patch_json( + self.get_host_url(self.controller.uuid), + [ + { + "path": constants.PTP_INSTANCE_ARRAY_PATH, + "value": ptp_instance_id, + "op": constants.PTP_PATCH_OPERATION_ADD + } + ], + headers=self.API_HEADERS, + ) + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.status_code, http_client.OK) + + return ptp_instance_id + + def test_host_ptp_instance_monitoring_assign_ok(self): + self._assign_host_ptp_instance_monitoring_success() + + def test_host_second_ptp_instance_monitoring_assign_failed(self): + self._assign_host_ptp_instance_monitoring_success() + + ptp_instance = dbutils.create_test_ptp_instance( + name="test-instance2", service=constants.PTP_INSTANCE_TYPE_MONITORING + ) + ptp_instance_id = ptp_instance["id"] + response = self.patch_json( + self.get_host_url(self.controller.uuid), + [ + { + "path": constants.PTP_INSTANCE_ARRAY_PATH, + "value": ptp_instance_id, + "op": constants.PTP_PATCH_OPERATION_ADD + } + ], + headers=self.API_HEADERS, + expect_errors=True, + ) + + self.assertEqual("application/json", response.content_type) + self.assertEqual(response.status_code, http_client.BAD_REQUEST) + self.assertIn( + "Monitoring ptp instance already exists on host", + response.json["error_message"] + ) + class TestGetPtpInstance(BasePtpInstanceTestCase): def setUp(self): @@ -172,7 +228,8 @@ class TestListPtpInstance(BasePtpInstanceTestCase): services = [constants.PTP_INSTANCE_TYPE_PTP4L, constants.PTP_INSTANCE_TYPE_PHC2SYS, constants.PTP_INSTANCE_TYPE_SYNCE4L, - constants.PTP_INSTANCE_TYPE_TS2PHC] + constants.PTP_INSTANCE_TYPE_TS2PHC, + constants.PTP_INSTANCE_TYPE_MONITORING] for service in services: name = '%s-%s' % (name_prefix, service) instance = dbutils.create_test_ptp_instance(name=name,