stages/chrony: support specifying refclocks

The refclock directive can be used to specify one or more hardware
reference clocks to be used as a time source.  Each refclock line must
specify a driver and a mandatory parameter, in the form:

  refclock driver parameter

Drivers can have driver-specific options:

  refclock driver parameter:[driver-option,...]

General refclock options can also be specified:

  refclock driver parameter:[driver-option,...] [general-option]...

The stage options schema is written so that the "driver" property is an
object that must match one of four schemas corresponding to the four
drivers, each with a "name" property matching the driver name.
Each driver defines its required property and any optional
driver-specific options.

There are more general refclock options supported than the ones defined
in this commit, but we can add them if and when we need them in the
future.

Note that the restriction on the top-level stage options schema is now
lifted and any set of options can be specified.  Servers are not
required.  However, at least one top-level property is required still.

Docs: https://chrony-project.org/doc/3.4/chrony.conf.html
This commit is contained in:
Achilleas Koutsou 2025-04-07 21:15:13 +02:00 committed by Simon de Vlieger
parent c9639c41f9
commit 8baf16da06
2 changed files with 273 additions and 14 deletions

View file

@ -54,6 +54,112 @@ def handle_leapsectz(chrony_conf_lines, timezone):
chrony_conf_lines[:] = [f"leapsectz {timezone}"] + chrony_conf_lines
def handle_refclocks(chrony_conf_lines, refclocks):
new_lines = []
for refclock in refclocks:
driver = refclock["driver"]
driver_name = driver["name"]
new_line = "refclock"
driver_options = ""
if driver_name == "PPS":
driver_options = handle_refclock_pps(driver)
elif driver_name == "SHM":
driver_options = handle_refclock_shm(driver)
elif driver_name == "SOCK":
driver_options = handle_refclock_sock(driver)
elif driver_name == "PHC":
driver_options = handle_refclock_phc(driver)
else:
# this should be caught by the schema
raise ValueError(f"unknown refclock driver {driver_name}")
new_line += " " + driver_options
# append general reflock options
refclock_options = []
poll = refclock.get("poll")
if poll is not None:
refclock_options.append(f"poll {poll}")
dpoll = refclock.get("dpoll")
if dpoll is not None:
refclock_options.append(f"dpoll {dpoll}")
offset = refclock.get("offset")
if offset is not None:
refclock_options.append(f"offset {offset}")
if refclock_options:
gen_options_str = " ".join(refclock_options)
new_line += " " + gen_options_str
new_lines.append(new_line)
chrony_conf_lines[:] = new_lines + chrony_conf_lines
def handle_refclock_pps(driver):
device = driver["device"]
line = f"PPS {device}"
options = []
if driver.get("clear"):
options.append("clear")
if options:
options_str = ",".join(options)
line += f":{options_str}"
return line
def handle_refclock_shm(driver):
segment = driver["segment"]
line = f"SHM {segment}"
options = []
perm = driver.get("perm")
if perm is not None:
options.append(f"perm={perm}")
if options:
options_str = ",".join(options)
line += f":{options_str}"
return line
def handle_refclock_sock(driver):
path = driver["path"]
return f"SOCK {path}"
def handle_refclock_phc(driver):
path = driver["path"]
line = f"PHC {path}"
options = []
if driver.get("nocrossts"):
options.append("nocrossts")
if driver.get("extpps"):
options.append("extpps")
pin = driver.get("pin")
if pin is not None:
options.append(f"pin={pin}")
channel = driver.get("channel")
if channel is not None:
options.append(f"channel={channel}")
if driver.get("clear"):
options.append("clear")
if options:
options_str = ",".join(options)
line += f":{options_str}"
return line
def main(tree, options):
timeservers = options.get("timeservers", [])
servers = options.get("servers", [])
@ -61,6 +167,8 @@ def main(tree, options):
# therefore default to 'None' to distinguish these two cases.
leapsectz = options.get("leapsectz", None)
refclocks = options.get("refclocks", [])
with open(f"{tree}/etc/chrony.conf", encoding="utf8") as f:
chrony_conf = f.read()
@ -76,6 +184,8 @@ def main(tree, options):
handle_servers(lines, servers)
if leapsectz is not None:
handle_leapsectz(lines, leapsectz)
if refclocks:
handle_refclocks(lines, refclocks)
new_chrony_conf = "\n".join(lines)

View file

@ -19,28 +19,19 @@
" - 'maxpoll'",
" - 'iburst' (defaults to true)",
" - 'prefer' (defaults to false)",
"",
"The `leapsectz` option configures chrony behavior related to automatic checking",
"of the next occurrence of the leap second, using the provided timezone. Its",
"value is a string representing a timezone from the system tz database (e.g.",
"'right/UTC'). If an empty string is provided, then all occurrences of",
"`leapsectz` directive are removed from the configuration.",
"Constraints:",
" - Exactly one of 'timeservers' or 'servers' options must be provided."
"",
"The refclock directive can be used to specify one or more hardware",
"reference clocks to be used as a time source."
],
"schema": {
"additionalProperties": false,
"oneOf": [
{
"required": [
"timeservers"
]
},
{
"required": [
"servers"
]
}
],
"minProperties": 1,
"properties": {
"timeservers": {
"type": "array",
@ -90,6 +81,164 @@
"leapsectz": {
"type": "string",
"description": "Timezone used by chronyd to determine when will the next leap second occur. Empty value will remove the option."
},
"refclocks": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"driver"
],
"properties": {
"driver": {
"oneOf": [
{
"$ref": "#/definitions/PPS"
},
{
"$ref": "#/definitions/SHM"
},
{
"$ref": "#/definitions/SOCK"
},
{
"$ref": "#/definitions/PHC"
}
]
},
"poll": {
"type": "integer",
"description": "Specifies the interval between processing times of timestamps as a power of 2 in seconds."
},
"dpoll": {
"type": "integer",
"description": "Some drivers do not listen for external events and try to produce samples in their own polling interval. This is defined as a power of 2 and can be negative to specify a sub-second interval. The default is 0 (1 second)."
},
"offset": {
"type": "number",
"description": "This option can be used to compensate for a constant error. The default is 0.0."
}
}
}
}
},
"definitions": {
"PPS": {
"description": "Driver for the kernel PPS (pulse per second) API.",
"type": "object",
"required": [
"name",
"device"
],
"additionalProperties": false,
"properties": {
"name": {
"type": "string",
"enum": [
"PPS"
]
},
"device": {
"type": "string",
"description": "Path to the PPS device (typically /dev/pps?).",
"pattern": "^\\/(?!\\.\\.)((?!\\/\\.\\.\\/).)+$"
},
"clear": {
"type": "boolean",
"description": "By default, the PPS refclock uses assert events (rising edge) for synchronisation. With this option, it will use clear events (falling edge) instead."
}
}
},
"SHM": {
"description": "NTP shared memory driver.",
"type": "object",
"required": [
"name",
"segment"
],
"additionalProperties": false,
"properties": {
"name": {
"type": "string",
"enum": [
"SHM"
]
},
"segment": {
"type": "integer",
"description": "The number of the shared memory segment."
},
"perm": {
"type": "string",
"pattern": "^[0-7]{4}$",
"description": "This option specifies the permissions of the shared memory segment created by chronyd. They are specified as a numeric mode. The default value is 0600 (read-write access for owner only)."
}
}
},
"SOCK": {
"description": "Unix domain socket driver.",
"type": "object",
"required": [
"name",
"path"
],
"additionalProperties": false,
"properties": {
"name": {
"type": "string",
"enum": [
"SOCK"
]
},
"path": {
"type": "string",
"pattern": "^\\/(?!\\.\\.)((?!\\/\\.\\.\\/).)+$",
"description": "The path to the socket."
}
}
},
"PHC": {
"description": "Unix domain socket driver.",
"type": "object",
"required": [
"name",
"path"
],
"additionalProperties": false,
"properties": {
"name": {
"type": "string",
"enum": [
"PHC"
]
},
"path": {
"type": "string",
"pattern": "^\\/(?!\\.\\.)((?!\\/\\.\\.\\/).)+$",
"description": "The path to the device of the PTP clock to be used as a time source."
},
"nocrossts": {
"type": "boolean",
"description": "Disable use of precise cross timestamping."
},
"extpps": {
"type": "boolean",
"description": "Enable a PPS mode in which the PTP clock is timestamping pulses of an external PPS signal connected to the clock."
},
"pin": {
"type": "integer",
"description": "The index of the pin for the PPS mode. The default value is 0."
},
"channel": {
"type": "integer",
"description": "The index of the channel for the PPS mode. The default value is 0."
},
"clear": {
"type": "boolean",
"description": "This option enables timestamping of clear events (falling edge) instead of assert events (rising edge) in the PPS mode. This may not work with some clocks."
}
}
}
}
}