Repository: sonic-net/sonic-buildimage
Pull request #22513: Enable SmartSwitch/NPU as a Chrony NTP server for DPU synchronization within chassis.
Author: login: vvolam
Created: 2025-05-01T20:44:39Z | Merged: 2025-05-09T18:03:29Z

<!--
     Please make sure you've read and understood our contributing guidelines:
     https://github.com/Azure/SONiC/blob/gh-pages/CONTRIBUTING.md

     ** Make sure all your commits include a signature generated with `git commit -s` **

     If this is a bug fix, make sure your description includes "fixes #xxxx", or
     "closes #xxxx" or "resolves #xxxx"

     Please provide the following information:
-->

#### Why I did it
To enable the NPU (SmartSwitch) as a Chrony NTP server for its connected DPUs, allowing them to synchronize time locally within the chassis environment. This is useful for closed environments where external NTP access may not be available.

##### Work item tracking
- Microsoft ADO **(number only)**: 32378864
- Github Issue **(number only)**:  https://github.com/sonic-net/sonic-buildimage/issues/22321

#### How I did it
- Modified chrony.conf.j2 to:
     Allow NTP server functionality and bind it to bridge-midplane only if DEVICE_METADATA.localhost.subtype == 'SmartSwitch' and NTP global server_role is enabled or dhcp is enabled.

- Updated config_samples.py:
     Added sample NTP and NTP_SERVER entries in generate_t1_smartswitch_switch_sample_config() and generate_t1_smartswitch_dpu_sample_config() functions to reflect the new use case.

#### How to verify it

<!--
If PR needs to be backported, then the PR must be tested against the base branch and the earliest backport release branch and provide tested image version on these two branches. For example, if the PR is requested for master, 202211 and 202012, then the requester needs to provide test results on master and 202012.
-->
- Built and deployed SONiC image with this change.
- Verified that chronyd starts and listens on the bridge-midplane interface on SmartSwitch.
- Verified NTP sync from DPU nodes.
- Validated configurations using show ntp.

NPU:
$ date
Tue May  6 03:53:15 PM UTC 2025

DPU:
$ show ntp
MGMT_VRF_CONFIG is not present.
Reference ID    : A9FEC8FE (169.254.200.254)
Stratum         : 4
Ref time (UTC)  : Mon May 05 21:44:58 2025
System time     : 0.000053240 seconds fast of NTP time
Last offset     : -0.051265407 seconds
RMS offset      : 0.049573295 seconds
Frequency       : 784.498 ppm slow
Residual freq   : -0.000 ppm
Skew            : 82.786 ppm
Root delay      : 0.047402713 seconds
Root dispersion : 0.048260666 seconds
Update interval : 64.9 seconds
Leap status     : Normal
MS Name/IP address         Stratum Poll Reach LastRx Last sample        
^* 169.254.200.254               3   6    77    10   -222us[  -51ms] +/-   70ms
$ date
Tue May  6 03:53:15 PM UTC 2025

#### Which release branch to backport (provide reason below if selected)

<!--
- Note we only backport fixes to a release branch, *not* features!
- Please also provide a reason for the backporting below.
- e.g.
- [x] 202006
-->

- [ ] 201811
- [ ] 201911
- [ ] 202006
- [ ] 202012
- [ ] 202106
- [ ] 202111
- [ ] 202205
- [ ] 202211
- [ ] 202305

#### Tested branch (Please provide the tested image version)

<!--
- Please provide tested image version
- e.g.
- [x] 20201231.100
-->

- [master] <!-- image version 1 -->

#### Description for the changelog
<!--
Write a short (one line) summary that describes the changes in this
pull request for inclusion in the changelog:
-->
Enable SmartSwitch/NPU as a Chrony NTP server for DPU synchronization within chassis.
<!--
 Ensure to add label/tag for the feature raised. example - PR#2174 under sonic-utilities repo. where, Generic Config and Update feature has been labelled as GCU.
-->

#### Link to config_db schema for YANG module changes
<!--
Provide a link to config_db schema for the table for which YANG model
is defined
Link should point to correct section on https://github.com/Azure/sonic-buildimage/blob/master/src/sonic-yang-models/doc/Configuration.md
-->

#### A picture of a cute animal (not mandatory but encouraged)




Diff:
diff --git a/files/image_config/chrony/chrony.conf.j2 b/files/image_config/chrony/chrony.conf.j2
index a41e6845158..e123653f6a3 100644
--- a/files/image_config/chrony/chrony.conf.j2
+++ b/files/image_config/chrony/chrony.conf.j2
@@ -12,6 +12,9 @@ confdir /etc/chrony/conf.d
 {# Getting NTP global configuration -#}
 {% set global = (NTP | d({})).get('global', {}) -%}
 
+{# Getting DEVICE_METADATA localhost configuration -#}
+{% set device_metadata = (DEVICE_METADATA | d({})).get('localhost', {}) -%}
+
 {# Adding NTP servers. We need to know if we have some pools, to set proper config -#}
 {% set ns = namespace(is_pools=false) %}
 {% for server in NTP_SERVER if NTP_SERVER[server].admin_state != 'disabled' -%}
@@ -51,10 +54,14 @@ confdir /etc/chrony/conf.d
 
 {% endfor -%}
 
+{# Enable the NTP server configuration only if the switch type is smartswitch -#}
+{% if device_metadata.subtype == 'SmartSwitch' -%}
 {# Enable NTP server functionality if server_role is enabled or DHCP configuration is enabled -#}
-{# {% if global.server_role == 'enabled' or global.dhcp == 'enabled' -%}
+{% if global.server_role == 'enabled' or global.dhcp == 'enabled' -%}
 allow
-{% endif -%} #}
+binddevice bridge-midplane
+{% endif -%}
+{% endif -%}
 
 {# use source interface if configured to send NTP requests, else use eth0 if running in mgmt
 vrf (default is not to listen on anything) -#}
diff --git a/src/sonic-config-engine/config_samples.py b/src/sonic-config-engine/config_samples.py
index d7ac6a5211a..a23613285a4 100644
--- a/src/sonic-config-engine/config_samples.py
+++ b/src/sonic-config-engine/config_samples.py
@@ -142,6 +142,12 @@ def generate_t1_smartswitch_switch_sample_config(data, ss_config):
 
         data['DHCP_SERVER_IPV4_PORT'] = dhcp_server_ports
 
+    data['NTP'] = {
+        "global": {
+            "server_role": "enabled"
+        }
+    }
+
     return data
 
 def generate_t1_smartswitch_dpu_sample_config(data, ss_config):
@@ -183,6 +189,12 @@ def generate_t1_smartswitch_dpu_sample_config(data, ss_config):
             }
         }
 
+    data['NTP_SERVER'] = {
+        "169.254.200.254": {
+            "iburst": "on"
+        }
+    }
+
     return data
 
 def generate_t1_smartswitch_sample_config(data):
diff --git a/src/sonic-config-engine/tests/data/ntp/ntp_smartswitch_interfaces.json b/src/sonic-config-engine/tests/data/ntp/ntp_smartswitch_interfaces.json
new file mode 100644
index 00000000000..a41d8f6542a
--- /dev/null
+++ b/src/sonic-config-engine/tests/data/ntp/ntp_smartswitch_interfaces.json
@@ -0,0 +1,60 @@
+{
+    "DEVICE_METADATA": {
+        "localhost": {
+            "type": "LeafRouter",
+            "subtype": "SmartSwitch"
+        }
+    },
+    "NTP": {
+        "global": {
+            "src_intf": "eth0",
+            "vrf": "default",
+            "authentication": "enabled",
+            "dhcp": "disabled",
+            "server_role": "enabled",
+            "admin_state": "enabled"
+        }
+    },
+    "NTP_SERVER": {
+        "my_ntp_server": {
+            "association_type": "server",
+            "iburst": "off",
+            "admin_state": "disabled",
+            "version": 3,
+            "resolve_as": "10.20.30.40"
+        },
+        "server2": {
+            "association_type": "server",
+            "iburst": "off",
+            "admin_state": "enabled",
+            "version": 3,
+            "resolve_as": "10.20.30.50",
+            "key": 42,
+            "trusted": "no"
+        },
+        "pool.ntp.org": {
+            "association_type": "pool",
+            "iburst": "on",
+            "admin_state": "enabled",
+            "version": 3,
+            "resolve_as": "pool.ntp.org"
+        }
+    },
+    "NTP_KEY": {
+        "1": {
+            "type": "md5",
+            "trusted": "no",
+            "value": "blabla"
+        },
+        "42": {
+            "type": "sha1",
+            "trusted": "yes",
+            "value": "the_answer"
+        }
+    },
+    "INTERFACE": {
+        "Ethernet0": {},
+        "Ethernet0|10.0.0.0/31": {},
+        "Ethernet0|192.168.0.122/24": {}
+    }
+}
diff --git a/src/sonic-config-engine/tests/sample_output/py2/chrony_smartswitch.conf b/src/sonic-config-engine/tests/sample_output/py2/chrony_smartswitch.conf
new file mode 120000
index 00000000000..44de6cfe08a
--- /dev/null
+++ b/src/sonic-config-engine/tests/sample_output/py2/chrony_smartswitch.conf
@@ -0,0 +1 @@
+../py3/chrony_smartswitch.conf
\ No newline at end of file
diff --git a/src/sonic-config-engine/tests/sample_output/py3/chrony.conf b/src/sonic-config-engine/tests/sample_output/py3/chrony.conf
index 539d08f34a8..1149cfeb86b 100644
--- a/src/sonic-config-engine/tests/sample_output/py3/chrony.conf
+++ b/src/sonic-config-engine/tests/sample_output/py3/chrony.conf
@@ -16,7 +16,6 @@ pool pool.ntp.org iburst version 3
 
 
 
-
 # Use time sources from DHCP.
 sourcedir /run/chrony-dhcp
 
diff --git a/src/sonic-config-engine/tests/sample_output/py3/chrony_smartswitch.conf b/src/sonic-config-engine/tests/sample_output/py3/chrony_smartswitch.conf
new file mode 100644
index 00000000000..c5b3f346d7a
--- /dev/null
+++ b/src/sonic-config-engine/tests/sample_output/py3/chrony_smartswitch.conf
@@ -0,0 +1,70 @@
+###############################################################################
+# This file was AUTOMATICALLY GENERATED. DO NOT MODIFY.
+# Controlled by chrony-config.sh
+###############################################################################
+
+# Welcome to the chrony configuration file. See chrony.conf(5) for more
+# information about usable directives.
+
+# Include configuration files found in /etc/chrony/conf.d.
+confdir /etc/chrony/conf.d
+
+server 10.20.30.50 key 42 iburst version 3
+
+pool pool.ntp.org iburst version 3
+
+allow
+binddevice bridge-midplane
+
+
+
+# Use time sources from DHCP.
+sourcedir /run/chrony-dhcp
+
+# Use NTP sources found in /etc/chrony/sources.d.
+sourcedir /etc/chrony/sources.d
+
+# This directive specify the location of the file containing ID/key pairs for
+# NTP authentication.
+keyfile /etc/chrony/chrony.keys
+
+# This directive specify the file into which chronyd will store the rate
+# information.
+driftfile /var/lib/chrony/chrony.drift
+
+# Save NTS keys and cookies.
+ntsdumpdir /var/lib/chrony
+
+# Uncomment the following line to turn logging on.
+#log tracking measurements statistics
+
+# Log files location.
+logdir /var/log/chrony
+
+# Stop bad estimates upsetting machine clock.
+maxupdateskew 100.0
+
+# This directive enables kernel synchronisation (every 11 minutes) of the
+# real-time clock. Note that it can’t be used along with the 'rtcfile' directive.
+#rtcsync
+
+# Instead of having the kernel manage the real-time clock, have chrony do this
+# instead. The reason for this is that if the system time and the real-time clock
+# are signficantly different from the actual time, then the system time must be
+# slewed, while the real-time clock can be stepped to the actual time. That way,
+# when the device next reboots (whether it be cold, warm, or fast), it will come
+# up with the actual time from the real-time clock.
+rtcfile /var/lib/chrony/rtc
+hwclockfile /etc/adjtime
+rtcautotrim 15
+
+# Step the system clock instead of slewing it if the adjustment is larger than
+# one second, but only in the first three clock updates.
+#
+# Disabled because we don't want chrony to do any clock steps; it should only slew
+#makestep 1 3
+
+# Get TAI-UTC offset and leap seconds from the system tz database.
+# This directive must be commented out when using time sources serving
+# leap-smeared time.
+leapsectz right/UTC
diff --git a/src/sonic-config-engine/tests/sample_output/t1-smartswitch-dpu.json b/src/sonic-config-engine/tests/sample_output/t1-smartswitch-dpu.json
index d9b52b9d7d8..c1c0d17847f 100644
--- a/src/sonic-config-engine/tests/sample_output/t1-smartswitch-dpu.json
+++ b/src/sonic-config-engine/tests/sample_output/t1-smartswitch-dpu.json
@@ -77,5 +77,10 @@
             "dash_ipv6_acl_rule_low_threshold": "70",
             "dash_ipv6_acl_rule_high_threshold": "85"
         }
+    },
+    "NTP_SERVER": {
+        "169.254.200.254": {
+            "iburst": "on"
+        }
     }
 }
diff --git a/src/sonic-config-engine/tests/sample_output/t1-smartswitch.json b/src/sonic-config-engine/tests/sample_output/t1-smartswitch.json
index 87036e755b9..fcc8e315d2c 100644
--- a/src/sonic-config-engine/tests/sample_output/t1-smartswitch.json
+++ b/src/sonic-config-engine/tests/sample_output/t1-smartswitch.json
@@ -675,5 +675,10 @@
 			"mtu": "9100",
 			"speed": "100000"
 		}
+	},
+	"NTP": {
+		"global": {
+			"server_role": "enabled"
+		}
 	}
 }
diff --git a/src/sonic-config-engine/tests/test_j2files.py b/src/sonic-config-engine/tests/test_j2files.py
index 51815e2729c..b2eb8aa7414 100644
--- a/src/sonic-config-engine/tests/test_j2files.py
+++ b/src/sonic-config-engine/tests/test_j2files.py
@@ -815,6 +815,15 @@ def test_ntp_conf(self):
         self.run_script(argument, output_file=self.output_file)
         assert utils.cmp(expected, self.output_file), self.run_diff(expected, self.output_file)
 
+    def test_ntp_smartswitch_conf(self):
+        conf_template = os.path.join(self.test_dir, "chrony.conf.j2")
+        config_db_ntp_json = os.path.join(self.test_dir, "data", "ntp", "ntp_smartswitch_interfaces.json")
+        expected = os.path.join(self.test_dir, "sample_output", utils.PYvX_DIR, "chrony_smartswitch.conf")
+
+        argument = ['-j', config_db_ntp_json, '-t', conf_template]
+        self.run_script(argument, output_file=self.output_file)
+        assert utils.cmp(expected, self.output_file), self.run_diff(expected, self.output_file)
+
     def test_ntp_keys(self):
         conf_template = os.path.join(self.test_dir, "chrony.keys.j2")
         config_db_ntp_json = os.path.join(self.test_dir, "data", "ntp", "ntp_interfaces.json")


Reviews:
user: login: SahilChaudhari
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T07:11:41Z
user: login: saiarcot895
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T15:23:35Z
user: login: saiarcot895
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T15:26:42Z
user: login: rameshraghupathy
body: @vvolam What is the time instant in the boot up process that the NTP sync happens?  Looks like once triggered the actual time sync happens in 2 mins.  What will be the time until time point?  Given this looks like the NTP sync should happen very early in the DPU boot up sequence.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T15:54:21Z
user: login: gpunathilell
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T17:26:54Z
user: login: vvolam
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T18:08:22Z
user: login: vvolam
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T19:04:15Z
user: login: vvolam
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T19:04:38Z
user: login: saiarcot895
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T19:06:24Z
user: login: gpunathilell
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T19:38:57Z
user: login: rameshraghupathy
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T17:39:51Z
user: login: rlhui
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T18:02:07Z

Review comments:
diff_hunk: @@ -51,10 +54,14 @@ confdir /etc/chrony/conf.d
 
 {% endfor -%}
 
-{# Enable NTP server functionality if server_role is enabled or DHCP configuration is enabled -#}
-{# {% if global.server_role == 'enabled' or global.dhcp == 'enabled' -%}
-allow
-{% endif -%} #}
+{# Enable the NTP server configuration only if the switch type is smartswitch -#}
+{% if device_metadata.subtype == 'SmartSwitch' -%}
+    {# Enable NTP server functionality if server_role is enabled or DHCP configuration is enabled -#}
+    {% if global.server_role == 'enabled' or global.dhcp == 'enabled' -%}
+        allow
+        binddevice bridge-midplane
path: files/image_config/chrony/chrony.conf.j2
user: login: saiarcot895
body: Can you not have any spaces at the beginning of these two lines? In the rendered conf file, visually speaking, the spaces would look out of place.
created_at: 2025-05-08T15:23:35Z
author_association: CONTRIBUTOR
path: src/sonic-config-engine/tests/sample_output/py3/chrony.conf
user: login: saiarcot895
body: Please add a test case in `src/sonic-config-engine/tests/test_j2files.py` verifying generation of chrony.conf, but for smartswitch and server mode enabled.
created_at: 2025-05-08T15:26:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -142,6 +142,12 @@ def generate_t1_smartswitch_switch_sample_config(data, ss_config):
 
         data['DHCP_SERVER_IPV4_PORT'] = dhcp_server_ports
 
+    data['NTP'] = {
path: src/sonic-config-engine/config_samples.py
user: login: gpunathilell
body: Would this work if there is already NTP related config in the default config? 
created_at: 2025-05-08T17:26:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -142,6 +142,12 @@ def generate_t1_smartswitch_switch_sample_config(data, ss_config):
 
         data['DHCP_SERVER_IPV4_PORT'] = dhcp_server_ports
 
+    data['NTP'] = {
path: src/sonic-config-engine/config_samples.py
user: login: vvolam
body: @gpunathilell I think yes, I will just set the server_role to be enabled based on my understanding.
created_at: 2025-05-08T18:08:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -51,10 +54,14 @@ confdir /etc/chrony/conf.d
 
 {% endfor -%}
 
-{# Enable NTP server functionality if server_role is enabled or DHCP configuration is enabled -#}
-{# {% if global.server_role == 'enabled' or global.dhcp == 'enabled' -%}
-allow
-{% endif -%} #}
+{# Enable the NTP server configuration only if the switch type is smartswitch -#}
+{% if device_metadata.subtype == 'SmartSwitch' -%}
+    {# Enable NTP server functionality if server_role is enabled or DHCP configuration is enabled -#}
+    {% if global.server_role == 'enabled' or global.dhcp == 'enabled' -%}
+        allow
+        binddevice bridge-midplane
path: files/image_config/chrony/chrony.conf.j2
user: login: vvolam
body: Removed it. Thank you!
created_at: 2025-05-08T19:04:15Z
author_association: CONTRIBUTOR
path: src/sonic-config-engine/tests/sample_output/py3/chrony.conf
user: login: vvolam
body: Added new test case for this scenario.. Thank you for this!
created_at: 2025-05-08T19:04:38Z
author_association: CONTRIBUTOR

PR comments:
user: login: mssonicbld
created_at: 2025-05-01T20:44:47Z
body: /azp run Azure.sonic-buildimage
author_association: COLLABORATOR
user: login: azure-pipelines[bot]
created_at: 2025-05-01T20:44:57Z
body: <samp>
Azure Pipelines successfully started running 1 pipeline(s).<br>

</samp>
author_association: NONE
user: login: mssonicbld
created_at: 2025-05-05T21:23:42Z
body: /azp run Azure.sonic-buildimage
author_association: COLLABORATOR
user: login: azure-pipelines[bot]
created_at: 2025-05-05T21:23:54Z
body: <samp>
Azure Pipelines successfully started running 1 pipeline(s).<br>

</samp>
author_association: NONE
user: login: rameshraghupathy
created_at: 2025-05-06T14:11:12Z
body: @vvolam Can you add your test results?
author_association: CONTRIBUTOR
user: login: vvolam
created_at: 2025-05-06T15:57:54Z
body: > @vvolam Can you add your test results?

@rameshraghupathy I have updated the date and show ntp outputs.
author_association: CONTRIBUTOR
user: login: mssonicbld
created_at: 2025-05-08T19:03:04Z
body: /azp run Azure.sonic-buildimage
author_association: COLLABORATOR
user: login: azure-pipelines[bot]
created_at: 2025-05-08T19:03:10Z
body: <samp>
Azure Pipelines will not run the associated pipelines, because the pull request was updated after the run command was issued. Review the pull request again and issue a new run command.<br>

</samp>
author_association: NONE
user: login: mssonicbld
created_at: 2025-05-08T19:03:16Z
body: /azp run Azure.sonic-buildimage
author_association: COLLABORATOR
user: login: azure-pipelines[bot]
created_at: 2025-05-08T19:03:31Z
body: <samp>
Azure Pipelines successfully started running 1 pipeline(s).<br>

</samp>
author_association: NONE
user: login: vvolam
created_at: 2025-05-08T19:23:24Z
body: > t? Given this looks like the NTP sync should happen very early in the DPU boot up sequence.

@rameshraghupathy, NTP synchronization should be utmost 30 secs with "iburst" on. 

When DPU is down, HW clock will be updated and, on the boot, chrony will synchronize with HW clock before it slews the clock from NTP server.

author_association: CONTRIBUTOR