Skip to content

Commit

Permalink
[NTP] Add NTP extended configuration (#15058)
Browse files Browse the repository at this point in the history
hld [#1296](sonic-net/SONiC#1296)
closes [#1254](sonic-net/SONiC#1254)
depends-on [#60](sonic-net/sonic-host-services#60), [#781](sonic-net/sonic-swss-common#781), [#2835](sonic-net/sonic-utilities#2835), [#10749](sonic-net/sonic-mgmt#10749)

#### Why I did it
To cover the next AIs:
* Configure NTP global parameters
* Add/remove new NTP servers
* Change the configuration for NTP servers
* Show NTP status
* Show NTP configuration

### How I did it
* Add YANG model for a new configuration
* Extend configuration templates to support new knobs

### Description for the changelog
* Add ability to configure NTP global parameters such as authentication, dhcp, admin state
* Change the configuration for NTP servers
* Add an ability to show NTP configuration

#### Link to config_db schema for YANG module changes
[NTP configuration](https://github.com/sonic-net/sonic-buildimage/blob/master/src/sonic-yang-models/doc/Configuration.md#ntp-and-syslog-servers)
  • Loading branch information
fastiuk authored Dec 11, 2023
1 parent b0bb3d4 commit 5efb123
Show file tree
Hide file tree
Showing 21 changed files with 888 additions and 244 deletions.
10 changes: 10 additions & 0 deletions files/build_templates/init_cfg.json.j2
Original file line number Diff line number Diff line change
Expand Up @@ -157,5 +157,15 @@
"memory": "0M-2G:256M,2G-4G:320M,4G-8G:384M,8G-:448M",
"num_dumps": "3"
}
},
"NTP": {
"global": {
"authentication": "disabled",
"dhcp": "enabled",
"server_role": "disabled",
"src_intf": "eth0",
"admin_state": "enabled",
"vrf": "default"
}
}
}
4 changes: 4 additions & 0 deletions files/build_templates/sonic_debian_extension.j2
Original file line number Diff line number Diff line change
Expand Up @@ -373,10 +373,14 @@ sudo dpkg --root=$FILESYSTEM_ROOT -i $debs_path/flashrom_*.deb
sudo cp -f $IMAGE_CONFIGS/cron.d/* $FILESYSTEM_ROOT/etc/cron.d/

# Copy NTP configuration files and templates
sudo LANG=C DEBIAN_FRONTEND=noninteractive chroot $FILESYSTEM_ROOT \
apt-get -y install ntpdate
sudo rm -f $FILESYSTEM_ROOT/etc/network/if-up.d/ntpsec-ntpdate
sudo cp $IMAGE_CONFIGS/ntp/ntp-config.service $FILESYSTEM_ROOT_USR_LIB_SYSTEMD_SYSTEM
echo "ntp-config.service" | sudo tee -a $GENERATED_SERVICE_FILE
sudo cp $IMAGE_CONFIGS/ntp/ntp-config.sh $FILESYSTEM_ROOT/usr/bin/
sudo cp $IMAGE_CONFIGS/ntp/ntp.conf.j2 $FILESYSTEM_ROOT_USR_SHARE_SONIC_TEMPLATES/
sudo cp $IMAGE_CONFIGS/ntp/ntp.keys.j2 $FILESYSTEM_ROOT_USR_SHARE_SONIC_TEMPLATES/
sudo cp $IMAGE_CONFIGS/ntp/ntp-systemd-wrapper $FILESYSTEM_ROOT/usr/libexec/ntpsec/
sudo mkdir $FILESYSTEM_ROOT_USR_LIB_SYSTEMD_SYSTEM/ntpsec.service.d
sudo cp $IMAGE_CONFIGS/ntp/sonic-target.conf $FILESYSTEM_ROOT_USR_LIB_SYSTEMD_SYSTEM/ntpsec.service.d/
Expand Down
2 changes: 2 additions & 0 deletions files/image_config/ntp/ntp-config.sh
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ function modify_ntp_default
}

sonic-cfggen -d -t /usr/share/sonic/templates/ntp.conf.j2 >/etc/ntpsec/ntp.conf
sonic-cfggen -d -t /usr/share/sonic/templates/ntp.keys.j2 >/etc/ntpsec/ntp.keys
chmod o-r /etc/ntp.keys

get_database_reboot_type
echo "Disabling NTP long jump for reboot type ${reboot_type} ..."
Expand Down
11 changes: 10 additions & 1 deletion files/image_config/ntp/ntp-systemd-wrapper
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ if [ -r /etc/default/ntpsec ]; then
. /etc/default/ntpsec
fi

if [ "$IGNORE_DHCP" != "yes" ] && [ -e /run/ntpsec/ntp.conf.dhcp ]; then
dhcp=$(/usr/local/bin/sonic-cfggen -d -v 'NTP["global"]["dhcp"]' 2> /dev/null)
if [ "$IGNORE_DHCP" != "yes" ] && [ -e /run/ntpsec/ntp.conf.dhcp ] && [ "$dhcp" = "enabled" ]; then
NTPD_OPTS="$NTPD_OPTS -c /run/ntpsec/ntp.conf.dhcp"
else
# List the default -c first, so if the admin has specified -c in
Expand All @@ -26,6 +27,14 @@ NTPD_OPTS="$NTPD_OPTS -u ntpsec:ntpsec"
# Protect the service startup against concurrent ntpdate ifup hooks
(
if flock -w 180 9; then
ntpEnabled=$(/usr/local/bin/sonic-cfggen -d -v 'NTP["global"]["admin_state"]' 2> /dev/null)
if [ "$ntpEnabled" = "disabled" ]
then
echo "Stopping NTP daemon"
kill -9 $(cat $PIDFILE)
exit 0
fi

# when mgmt vrf is configured, ntp starts in mgmt vrf by default unless user configures otherwise
vrfEnabled=$(/usr/local/bin/sonic-cfggen -d -v 'MGMT_VRF_CONFIG["vrf_global"]["mgmtVrfEnabled"]' 2> /dev/null)
vrfConfigured=$(/usr/local/bin/sonic-cfggen -d -v 'NTP["global"]["vrf"]' 2> /dev/null)
Expand Down
126 changes: 88 additions & 38 deletions files/image_config/ntp/ntp.conf.j2
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
###############################################################################
# Managed by Ansible
# file: ansible/roles/acs/templates/ntp.conf.j2
# This file was AUTOMATICALLY GENERATED. DO NOT MODIFY.
# Controlled by ntp-config.service
###############################################################################

# /etc/ntpsec/ntp.conf, configuration for ntpd; see ntp.conf(5) for help
# /etc/ntp.conf, configuration for ntpd; see ntp.conf(5) for help

# To avoid ntpd from panic and exit if the drift between new time and
# current system time is large.
Expand All @@ -12,35 +12,82 @@ tinker panic 0
driftfile /var/lib/ntpsec/ntp.drift
leapfile /usr/share/zoneinfo/leap-seconds.list

# To enable Network Time Security support as a server, obtain a certificate
# (e.g. with Let's Encrypt), configure the paths below, and uncomment:
# nts cert CERT_FILE
# nts key KEY_FILE
# nts enable

# You must create /var/log/ntpsec (owned by ntpsec:ntpsec) to enable logging.
#statsdir /var/log/ntpsec/
#statistics loopstats peerstats clockstats
#filegen loopstats file loopstats type day enable
#filegen peerstats file peerstats type day enable
#filegen clockstats file clockstats type day enable

# Specify one or more NTP servers.

# Public NTP servers supporting Network Time Security:
# server time.cloudflare.com nts
{% for ntp_server in NTP_SERVER %}
server {{ ntp_server }} iburst
{# Getting NTP global configuration -#}
{% set global = (NTP | d({})).get('global', {}) -%}

{# 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' and
NTP_SERVER[server].resolve_as and
NTP_SERVER[server].association_type -%}
{% set config = NTP_SERVER[server] -%}
{# Server options -#}
{% set soptions = '' -%}
{# Server access control options -#}
{% set aoptions = '' -%}

{# Authentication key -#}
{% if global.authentication == 'enabled' -%}
{% if config.key -%}
{% set soptions = soptions ~ ' key ' ~ config.key -%}
{% endif -%}
{% endif -%}

{# Aggressive polling -#}
{% if config.iburst -%}
{% set soptions = soptions ~ ' iburst' -%}
{% endif -%}

{# Protocol version -#}
{% if config.version -%}
{% set soptions = soptions ~ ' version ' ~ config.version -%}
{% endif -%}

{# Check if there are any pool configured. BTW it doesn't matter what was
configured as "resolve_as" for pools. If they were configured with FQDN they
must remain like that -#}
{% set config_as = config.resolve_as -%}
{% if config.association_type == 'pool' -%}
{% set ns.is_pools = true -%}
{% set config_as = server -%}
{% else -%}
{% set aoptions = aoptions ~ ' nopeer' -%}
{% endif -%}

{{ config.association_type }} {{ config_as }}{{ soptions }}
{% if global.server_role == 'disabled' %}
restrict {{ config_as }} kod limited nomodify notrap noquery{{ aoptions }}
{% endif %}

{% endfor -%}

{% set trusted_keys_arr = [] -%}
{% for key in NTP_KEY -%}
{% set keydata = NTP_KEY[key] -%}
{% if keydata.trusted == 'yes' -%}
{% set trusted_keys_arr = trusted_keys_arr.append(key) -%}
{% endif -%}
{% endfor %}

# pool.ntp.org maps to about 1000 low-stratum NTP servers. Your server will
# pick a different set every time it starts up. Please consider joining the
# pool: <https://www.pool.ntp.org/join.html>
{% if global.authentication == 'enabled' %}
keys /etc/ntpsec/ntp.keys
{% if trusted_keys_arr != [] %}
trustedkey {{ trusted_keys_arr|join(' ') }}
{% endif %}
{% endif %}

#listen on source interface if configured, else
#only listen on MGMT_INTERFACE, LOOPBACK_INTERFACE ip when MGMT_INTERFACE is not defined, or eth0
# if we don't have both of them (default is to listen on all ip addresses)
{# listen on source interface if configured, else only listen on MGMT_INTERFACE,
LOOPBACK_INTERFACE ip when MGMT_INTERFACE is not defined, or eth0 if we don't
have both of them (default is to listen on all ip addresses) -#}
interface ignore wildcard

{# Set interface to listen on:
* Set global variable for configured source interface name.
* Set global boolean to indicate if the ip of the configured source
interface is configured.
* If the source interface is configured but no ip on that
interface, then listen on another interface based on existing logic. -#}
{%- macro check_ip_on_interface(interface_name, table_name) %}
{%- set ns = namespace(valid_intf = 'false') %}
{%- if table_name %}
Expand All @@ -55,8 +102,8 @@ interface ignore wildcard

{% set ns = namespace(source_intf = "") %}
{%- set ns = namespace(source_intf_ip = 'false') %}
{%- if (NTP) and (NTP['global']['src_intf']) %}
{%- set ns.source_intf = (NTP['global']['src_intf']) %}
{%- if global.src_intf %}
{%- set ns.source_intf = global.src_intf %}
{%- if ns.source_intf != "" %}
{%- if ns.source_intf == "eth0" %}
{%- set ns.source_intf_ip = 'true' %}
Expand Down Expand Up @@ -91,16 +138,19 @@ interface listen eth0
{% endif %}
interface listen 127.0.0.1

# Access control configuration; see /usr/share/doc/ntpsec-doc/html/accopt.html
# for details.
#
# Note that "restrict" applies to both servers and clients, so a configuration
# that might be intended to block requests from certain clients could also end
# up blocking replies from your own upstream servers.
{# Access control options -#}
{% set options = '' -%}

{# Disable NTP server functionality. Should stay on when dhcp is enabled -#}
{# {% if global.server_role == 'disabled' and global.dhcp == 'disabled' -%}
{% set options = options ~ ' ignore' -%}
{% endif -%} #}

# Access control configuration
# By default, exchange time with everybody, but don't allow configuration.
# NTPsec doesn't establish peer associations, and so nopeer has no effect, and has been removed from here
restrict default kod nomodify noquery limited
# NTPsec doesn't establish peer associations, and so nopeer has no effect, and
# has been removed from here
restrict default kod nomodify noquery limited{{ options }}

# Local users may interrogate the ntp server more closely.
restrict 127.0.0.1
Expand Down
18 changes: 18 additions & 0 deletions files/image_config/ntp/ntp.keys.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
###############################################################################
# This file was AUTOMATICALLY GENERATED. DO NOT MODIFY.
# Controlled by ntp-config.service
###############################################################################

{# We can connect only to the servers we trust. Determine those servers -#}
{% set trusted_arr = [] -%}
{% for server in NTP_SERVER if NTP_SERVER[server].trusted == 'yes' and
NTP_SERVER[server].resolve_as -%}
{% set _ = trusted_arr.append(NTP_SERVER[server].resolve_as) -%}
{% endfor -%}

{# Define authentication keys inventory -#}
{% set trusted_str = ' ' ~ trusted_arr|join(',') -%}
{% for keyid in NTP_KEY if NTP_KEY[keyid].type and NTP_KEY[keyid].value %}
{% set keyval = NTP_KEY[keyid].value | b64decode %}
{{ keyid }} {{ NTP_KEY[keyid].type }} {{ keyval }}{{trusted_str}}
{% endfor -%}
100 changes: 0 additions & 100 deletions files/image_config/ntp/ntpsec

This file was deleted.

28 changes: 28 additions & 0 deletions src/sonic-config-engine/sonic-cfggen
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import os
import sys
import yaml
import ipaddress
import base64

from collections import OrderedDict
from config_samples import generate_sample_config, get_available_config
Expand Down Expand Up @@ -139,6 +140,29 @@ def ip_network(value):
return "Invalid ip address %s" % value
return r_v.network

def b64encode(value):
"""Base64 encoder
Return:
encoded string or the same value in case of error
"""
try:
ret = base64.b64encode(value.encode()).decode()
except:
return value
return ret


def b64decode(value):
"""Base64 decoder
Return:
decoded string or the same value in case of error
"""
try:
ret = base64.b64decode(value.encode()).decode()
except:
return value
return ret

def get_primary_addr(value):
if not value:
return ""
Expand Down Expand Up @@ -274,6 +298,10 @@ def _get_jinja2_env(paths):
for attr in ['ip', 'network', 'prefixlen', 'netmask', 'broadcast']:
env.filters[attr] = partial(prefix_attr, attr)

# Base64 encoder/decoder
env.filters['b64encode'] = b64encode
env.filters['b64decode'] = b64decode

return env

def main():
Expand Down
Loading

0 comments on commit 5efb123

Please sign in to comment.