Skip to content

Commit 1ee40bf

Browse files
authored
Add host.get_ntp_synchronized and host.set_servertime (#6743)
**host.get_ntp_synchronized**: Simply return true or false by parsing "System clock synchronized" or "NTP synchronized" from the output of "timedatectl status" like the following, no matter the NTP being enabled or not. ``` # timedatectl status Local time: Wed 2025-11-05 06:10:42 UTC Universal time: Wed 2025-11-05 06:10:42 UTC RTC time: Wed 2025-11-05 05:00:11 Time zone: UTC (UTC, +0000) System clock synchronized: yes NTP service: active RTC in local TZ: no ``` **host.set_servertime**: Use "timedatectl set-time" to set local time on the host in its local timezone only when NTP is disabled.
2 parents 4a67e13 + 9b674e9 commit 1ee40bf

File tree

10 files changed

+112
-0
lines changed

10 files changed

+112
-0
lines changed

ocaml/idl/datamodel_errors.ml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2065,6 +2065,9 @@ let _ =
20652065
error Api_errors.invalid_ntp_config ["reason"]
20662066
~doc:"The NTP configuration is invalid." () ;
20672067

2068+
error Api_errors.not_allowed_when_ntp_is_enabled ["host"]
2069+
~doc:"The operation is not allowed on the host when the NTP is enabled." () ;
2070+
20682071
message
20692072
(fst Api_messages.ha_pool_overcommitted)
20702073
~doc:

ocaml/idl/datamodel_host.ml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2646,6 +2646,33 @@ let list_timezones =
26462646
~result:(Set String, "The set of available timezones on the host")
26472647
~allowed_roles:_R_READ_ONLY ()
26482648

2649+
let get_ntp_synchronized =
2650+
call ~name:"get_ntp_synchronized" ~lifecycle:[]
2651+
~doc:
2652+
"Returns true if the system clock on the host is synchronized with the \
2653+
NTP servers."
2654+
~params:[(Ref _host, "self", "The host")]
2655+
~result:
2656+
( Bool
2657+
, "true if the system clock on the host is synchronized with the NTP \
2658+
servers."
2659+
)
2660+
~allowed_roles:_R_READ_ONLY ()
2661+
2662+
let set_servertime =
2663+
call ~name:"set_servertime" ~lifecycle:[]
2664+
~doc:"Set the host's system clock when NTP is disabled."
2665+
~params:
2666+
[
2667+
(Ref _host, "self", "The host")
2668+
; ( DateTime
2669+
, "value"
2670+
, "A date/time to be set. When a timezone offset is missing, UTC is \
2671+
assumed."
2672+
)
2673+
]
2674+
~allowed_roles:_R_POOL_OP ()
2675+
26492676
(** Hosts *)
26502677
let t =
26512678
create_obj ~in_db:true
@@ -2800,6 +2827,8 @@ let t =
28002827
; get_ntp_servers_status
28012828
; set_timezone
28022829
; list_timezones
2830+
; get_ntp_synchronized
2831+
; set_servertime
28032832
]
28042833
~contents:
28052834
([

ocaml/libs/clock/date.ml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,8 @@ let strip_tz tz t =
150150
in
151151
{t; tz= None}
152152

153+
let to_utc {t; tz} = Option.bind tz (fun _ -> Some {t; tz= utc})
154+
153155
let _localtime_string tz t = strip_tz tz t |> to_rfc3339
154156

155157
let localtime () =

ocaml/libs/clock/date.mli

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,7 @@ val is_later : than:t -> t -> bool
8686
val diff : t -> t -> Ptime.Span.t
8787
(** [diff a b] returns the span of time corresponding to [a - b]. When [than]
8888
or [b] lack a timezone, UTC is assumed. *)
89+
90+
val to_utc : t -> t option
91+
(** [to_utc a] returns [Some b] where [b] is equivalent to [a] but in UTC, if
92+
[a] has a timezone offset [Some tz]. Otherwise, the return is [None]. *)

ocaml/xapi-consts/api_errors.ml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1440,3 +1440,6 @@ let tls_verification_not_enabled_in_pool =
14401440
let sysprep = add_error "SYSPREP"
14411441

14421442
let invalid_ntp_config = add_error "INVALID_NTP_CONFIG"
1443+
1444+
let not_allowed_when_ntp_is_enabled =
1445+
add_error "NOT_ALLOWED_WHEN_NTP_IS_ENABLED"

ocaml/xapi/message_forwarding.ml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4184,6 +4184,26 @@ functor
41844184
let local_fn = Local.Host.list_timezones ~self in
41854185
let remote_fn = Client.Host.list_timezones ~self in
41864186
do_op_on ~local_fn ~__context ~host:self ~remote_fn
4187+
4188+
let get_ntp_synchronized ~__context ~self =
4189+
info "Host.get_ntp_synchronized: host = '%s'" (host_uuid ~__context self) ;
4190+
let local_fn = Local.Host.get_ntp_synchronized ~self in
4191+
let remote_fn = Client.Host.get_ntp_synchronized ~self in
4192+
do_op_on ~local_fn ~__context ~host:self ~remote_fn
4193+
4194+
let set_servertime ~__context ~self ~value =
4195+
info "Host.set_servertime : host = '%s'; value = '%s'"
4196+
(host_uuid ~__context self)
4197+
(Clock.Date.to_rfc3339 value) ;
4198+
if Db.Host.get_ntp_enabled ~__context ~self then
4199+
raise
4200+
(Api_errors.Server_error
4201+
(Api_errors.not_allowed_when_ntp_is_enabled, [Ref.string_of self])
4202+
)
4203+
else
4204+
let local_fn = Local.Host.set_servertime ~self ~value in
4205+
let remote_fn = Client.Host.set_servertime ~self ~value in
4206+
do_op_on ~local_fn ~__context ~host:self ~remote_fn
41874207
end
41884208

41894209
module Host_crashdump = struct

ocaml/xapi/xapi_host.ml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3562,8 +3562,12 @@ let get_ntp_servers_status ~__context ~self:_ =
35623562
else
35633563
[]
35643564

3565+
(* Prevent concurrent time/timezeone operations from interleaving. *)
3566+
let time_m = Mutex.create ()
3567+
35653568
let set_timezone ~__context ~self ~value =
35663569
try
3570+
with_lock time_m @@ fun () ->
35673571
let _ =
35683572
Helpers.call_script !Xapi_globs.timedatectl ["set-timezone"; value]
35693573
in
@@ -3581,3 +3585,34 @@ let list_timezones ~__context ~self:_ =
35813585
Helpers.call_script !Xapi_globs.timedatectl ["list-timezones"]
35823586
|> Astring.String.cuts ~empty:false ~sep:"\n"
35833587
with e -> Helpers.internal_error "%s" (ExnHelper.string_of_exn e)
3588+
3589+
let get_ntp_synchronized ~__context ~self:_ =
3590+
match Xapi_host_ntp.is_synchronized () with
3591+
| Ok r ->
3592+
r
3593+
| Error msg ->
3594+
Helpers.internal_error "%s" msg
3595+
3596+
let set_servertime ~__context ~self ~value =
3597+
let f = Helpers.call_script !Xapi_globs.timedatectl in
3598+
with_lock time_m @@ fun () ->
3599+
let tz = Db.Host.get_timezone ~__context ~self in
3600+
let not_utc = tz <> "UTC" in
3601+
try
3602+
(* The [value] stores only a naive date/time and a fixed timezone offset.
3603+
Convert it to UTC to avoid ambiguities caused by additional timezone
3604+
details, such as Daylight Saving Time (DST) rules or historical changes. *)
3605+
match Date.to_utc value |> Option.map Date.to_ptime with
3606+
| Some t ->
3607+
let (y, mon, d), ((h, min, s), _) = Ptime.to_date_time t in
3608+
let naive_in_utc =
3609+
Printf.sprintf "%04i-%02i-%02i %02i:%02i:%02i" y mon d h min s
3610+
in
3611+
if not_utc then (f ["set-timezone"; "UTC"] : string) |> ignore ;
3612+
(f ["set-time"; naive_in_utc] : string) |> ignore ;
3613+
if not_utc then (f ["set-timezone"; tz] : string) |> ignore ;
3614+
debug "%s: %s" __FUNCTION__ (f ["status"])
3615+
| None ->
3616+
raise (Invalid_argument "Missing timezone offset in value")
3617+
with e ->
3618+
Helpers.internal_error "%s: %s" __FUNCTION__ (ExnHelper.string_of_exn e)

ocaml/xapi/xapi_host.mli

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -632,3 +632,8 @@ val set_timezone :
632632
__context:Context.t -> self:API.ref_host -> value:string -> unit
633633

634634
val list_timezones : __context:Context.t -> self:API.ref_host -> string list
635+
636+
val get_ntp_synchronized : __context:Context.t -> self:API.ref_host -> bool
637+
638+
val set_servertime :
639+
__context:Context.t -> self:API.ref_host -> value:Clock.Date.t -> unit

ocaml/xapi/xapi_host_ntp.ml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,3 +175,12 @@ let promote_legacy_default_servers () =
175175
set_servers_in_conf defaults ;
176176
restart_ntp_service ()
177177
)
178+
179+
let is_synchronized () =
180+
let patterns = ["System clock synchronized: yes"; "NTP synchronized: yes"] in
181+
try
182+
Helpers.call_script !Xapi_globs.timedatectl ["status"]
183+
|> String.split_on_char '\n'
184+
|> List.exists ((Fun.flip List.mem) patterns)
185+
|> Result.ok
186+
with e -> Error (ExnHelper.string_of_exn e)

ocaml/xapi/xapi_host_ntp.mli

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,5 @@ val get_servers_from_conf : unit -> string list
3535
val is_ntp_dhcp_enabled : unit -> bool
3636

3737
val get_servers_status : unit -> (string * string) list
38+
39+
val is_synchronized : unit -> (bool, string) Result.t

0 commit comments

Comments
 (0)