Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions ocaml/idl/datamodel_errors.ml
Original file line number Diff line number Diff line change
Expand Up @@ -2065,6 +2065,12 @@ let _ =
error Api_errors.invalid_ntp_config ["reason"]
~doc:"The NTP configuration is invalid." () ;

error Api_errors.not_allowed_when_ntp_is_enabled ["host"]
~doc:"The operation is not allowed on the host when the NTP is enabled." () ;

error Api_errors.not_allowed_tz_in_localtime []
~doc:"The timezone is not allowed in localtime." () ;

message
(fst Api_messages.ha_pool_overcommitted)
~doc:
Expand Down
30 changes: 30 additions & 0 deletions ocaml/idl/datamodel_host.ml
Original file line number Diff line number Diff line change
Expand Up @@ -2627,6 +2627,34 @@ let get_ntp_servers_status =
)
~allowed_roles:_R_READ_ONLY ()

let get_ntp_synchronized =
call ~name:"get_ntp_synchronized" ~lifecycle:[]
~doc:
"Returns true if the system clock on the host is synchronized with the \
NTP servers."
~params:[(Ref _host, "self", "The host")]
~result:
( Bool
, "true if the system clock on the host is synchronized with the NTP \
servers."
)
~allowed_roles:_R_READ_ONLY ()

let set_server_localtime =
call ~name:"set_server_localtime" ~lifecycle:[]
~doc:
"Set the host's system clock in its local timezone when NTP is disabled."
Comment on lines +2643 to +2646
Copy link
Member

@psafont psafont Nov 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using localtime is confusing because it's used for a very specific thing in UNIX: time + timezone, but this call is _not_setting the timezone. Please use something that doesn't use the term localtime, for example:

Suggested change
let set_server_localtime =
call ~name:"set_server_localtime" ~lifecycle:[]
~doc:
"Set the host's system clock in its local timezone when NTP is disabled."
call ~name:"set_time" ~lifecycle:[]
~doc:
"Set the host's system clock, errors out when NTP is enabled."

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There will be timezone management in the following PR, see the feature branch name.
The ntp can be disabled, so offer an api to set host local time.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is exactly the writer regarding the existing reader: host.get_server_localtime, which returning only time without timezone.
I would think it should be used together with host.set_timezone.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There will be timezone management in the following PR, see the feature branch name.
The ntp can be disabled, so offer an api to set host local time.

This is a feature and it's missing a design a requirements, we've talked about this before :(

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK. I will push the feature user cases and design later.

~params:
[
(Ref _host, "self", "The host")
; ( DateTime
, "value"
, "A datetime without timezone information. If UTC is specified, the \
timezone will be ignored."
)
Comment on lines +2652 to +2654
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's not enough information in the commit message to understand the requirements of set_server_localtime. Please explain your decisions in there.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the writer function for localtime, which has an existing reader function host.get_server_localtime. So it is a datetime without timezone as the timezone is managed via other APIs.
For example, a local time + local timeznoe is 2025-11-05T11:00:00+08:00, the datetime here is just 2025-11-05T11:00:00. The timezone +08:00 will be managed separately.

Copy link
Member

@psafont psafont Nov 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get_server_locatime's way of serving information is outright dangerous, please treat it as a deprecated function that should not be used. I'd rather have a new function to get the host's server time to accompany the new function being introduced.

In particular the function is dangerous because it does not show the timezone, a correct function must return the time in this format: 2025-11-05T11:00:00+08:00.

I raise the the issue about lack of requirements is there because we need to know what the user needs to do, exactly. For example:

  • Set the correct time (only)
  • Set the timezone so the server is able to change the time difference when the timezone tables say so.

Note that the second one means that the timezone data needs to be packaged and updated periodically, which I don't think xenserver nor xcp-ng do.

My recommendation would be to avoid letting users setting the timezone for the server. UTC is more than enough and avoid complexity.

Fortunately the time can be set always correctly, even if the user provides a timestamp with a timezone difference like +07:45, so I would stick with that.

Copy link
Member Author

@minglumlu minglumlu Nov 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

avoid letting users setting the timezone for the server.

I'm afraid this is a valid request and use case which can't be avoided.

I'd rather have a new function to get the host's server time to accompany the new function being introduced.

Generally I agree to set the time + timezone together always. But the existing two APIs:
host.get_servertime which returns time + UTC, and
host.get_server_localtime which returns time only,
make me feel that I should be careful on introducing new function. I would prefer to let the new setter function paired with one of two above, instead of introducing a new getter function.

Copy link
Member Author

@minglumlu minglumlu Nov 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. I think I can change the implementation of host.get_servertime to make it return time + timezone. The doc of the API doesn't say it will always return UTC although it does currently.
So the setter API could be named as host.set_servertime and accept time + timezone.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@psafont May I please know your thoughts on above proposal?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. I think I can change the implementation of host.get_servertime to make it return time + timezone. The doc of the API doesn't say it will always return UTC although it does currently.
So the setter API could be named as host.set_servertime and accept time + timezone.

This is what I would prefer as well.

I probably would also add two other timezone-related functions:

  • list available timezones
  • set a timezone

I have to repeat that I would prefer if the server was always in UTC, it can disrupts timestamps in logs and other debug information, which can be confusing. I have not found a real need to set a concrete timezone, this is because API clients can convert times to the timezone of the machine the user is using.

Copy link
Member Author

@minglumlu minglumlu Nov 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is what I would prefer as well.

Thanks.

I have to repeat that I would prefer if the server was always in UTC, ..., I have not found a real need to set a concrete timezone, this is because API clients can convert time to the timezone of the machine the user is using.

There are real user's requests for setting timezone on servers. And a user who doesn't want to set timezone can ignore the new setting APIs.

it can disrupts timestamps in logs and other debug information, which can be confusing.

IMO, this is the unavoidable consequence of the user's action of setting timezone.

]
~allowed_roles:_R_POOL_OP ()

(** Hosts *)
let t =
create_obj ~in_db:true
Expand Down Expand Up @@ -2779,6 +2807,8 @@ let t =
; disable_ntp
; enable_ntp
; get_ntp_servers_status
; get_ntp_synchronized
; set_server_localtime
]
~contents:
([
Expand Down
4 changes: 3 additions & 1 deletion ocaml/libs/clock/date.mli
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@
datetime string. This timezone is determined when creating a value and
cannot be changed. For timestamps created from datetime strings, the
timezone is maintained. For all other values UTC is used. *)
type t
type tz = int option

type t = {t: Ptime.t; tz: tz}

(** Conversions *)

Expand Down
5 changes: 5 additions & 0 deletions ocaml/xapi-consts/api_errors.ml
Original file line number Diff line number Diff line change
Expand Up @@ -1440,3 +1440,8 @@ let tls_verification_not_enabled_in_pool =
let sysprep = add_error "SYSPREP"

let invalid_ntp_config = add_error "INVALID_NTP_CONFIG"

let not_allowed_when_ntp_is_enabled =
add_error "NOT_ALLOWED_WHEN_NTP_IS_ENABLED"

let not_allowed_tz_in_localtime = add_error "NOT_ALLOWED_TZ_IN_LOCALTIME"
20 changes: 20 additions & 0 deletions ocaml/xapi/message_forwarding.ml
Original file line number Diff line number Diff line change
Expand Up @@ -4170,6 +4170,26 @@ functor
let local_fn = Local.Host.get_ntp_servers_status ~self in
let remote_fn = Client.Host.get_ntp_servers_status ~self in
do_op_on ~local_fn ~__context ~host:self ~remote_fn

let get_ntp_synchronized ~__context ~self =
info "Host.get_ntp_synchronized: host = '%s'" (host_uuid ~__context self) ;
let local_fn = Local.Host.get_ntp_synchronized ~self in
let remote_fn = Client.Host.get_ntp_synchronized ~self in
do_op_on ~local_fn ~__context ~host:self ~remote_fn

let set_server_localtime ~__context ~self ~value =
info "Host.set_server_localtime: host = '%s'; value = '%s'"
(host_uuid ~__context self)
(Clock.Date.to_rfc3339 value) ;
if Db.Host.get_ntp_enabled ~__context ~self then
raise
(Api_errors.Server_error
(Api_errors.not_allowed_when_ntp_is_enabled, [Ref.string_of self])
)
else
let local_fn = Local.Host.set_server_localtime ~self ~value in
let remote_fn = Client.Host.set_server_localtime ~self ~value in
do_op_on ~local_fn ~__context ~host:self ~remote_fn
end

module Host_crashdump = struct
Expand Down
7 changes: 7 additions & 0 deletions ocaml/xapi/xapi_globs.ml
Original file line number Diff line number Diff line change
Expand Up @@ -812,6 +812,8 @@ let ntp_dhcp_dir = ref "/run/chrony-dhcp"

let ntp_client_path = ref "/usr/bin/chronyc"

let timedatectl = ref "/usr/bin/timedatectl"

let udhcpd_skel = ref (Filename.concat "/etc/xensource" "udhcpd.skel")

let udhcpd_leases_db = ref "/var/lib/xcp/dhcp-leases.db"
Expand Down Expand Up @@ -1904,6 +1906,11 @@ let other_options =
, (fun () -> !ntp_client_path)
, "Path to the ntp client binary"
)
; ( "timedatectl"
, Arg.Set_string timedatectl
, (fun () -> !timedatectl)
, "Path to the timedatectl executable"
)
; gen_list_option "legacy-default-ntp-servers"
"space-separated list of legacy default NTP servers"
(fun s -> s)
Expand Down
25 changes: 25 additions & 0 deletions ocaml/xapi/xapi_host.ml
Original file line number Diff line number Diff line change
Expand Up @@ -3555,3 +3555,28 @@ let get_ntp_servers_status ~__context ~self:_ =
Xapi_host_ntp.get_servers_status ()
else
[]

let get_ntp_synchronized ~__context ~self:_ =
match Xapi_host_ntp.is_synchronized () with
| Ok r ->
r
| Error msg ->
Helpers.internal_error "%s" msg

let set_server_localtime ~__context ~self:_ ~value =
let param =
match value with
| Date.{t; tz= Some 0} | Date.{t; tz= None} ->
(* Ideally it should be of a new type like NaiveDateTime. For
simplicity, reuse DateTime here. But it can't tell if the UTC is
specified explicitly or not. Just ignore it in that case. *)
let (y, mon, d), ((h, min, s), _) = Ptime.to_date_time t in
Printf.sprintf "%04i-%02i-%02i %02i:%02i:%02i" y mon d h min s
| _ ->
raise
(Api_errors.Server_error (Api_errors.not_allowed_tz_in_localtime, []))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't quite understand the timezone handlling here. I expected the time format "YYYY-MM-DD HH:MM:SS" which is localtime. It should have no business with timezone. Where does the timezone come from? The DateTime type?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. The DateTime contains timezone.

Copy link
Contributor

@changlei-li changlei-li Nov 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then why can't distinguish the local time and UTC.
20251105T16:11:55 is local time without tz,
20251105T08:11:55Z is UTC time with tz=0,
We need the tz = None branch is enough, isn't it right?

Copy link
Member Author

@minglumlu minglumlu Nov 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API uses best_effort_iso8601_to_rfc3339 to assign a UTC if the datetime argument doesn't contain any timezone information. This happens in deserialization automatically.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I see

Comment on lines +3567 to +3577
Copy link
Member

@psafont psafont Nov 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is all this code needed? Reading the manual of timedatectl, it should be able to accept any date in RFC3339 format, see man 7 systemd.time.

Additionally, datetimes without a timezone reference must be refused, and not assumed to be UTC. We've had this issue in xapi for a very long long time, I don't want to perpetuate it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But we hope to accept a local time without timezone as there is another API to set the timezone.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But we hope to accept a local time without timezone as there is another API to set the timezone.

That API is old and outright dangerous, please treat it as deprecated. A new, sane API function should be exposed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe better not reuse the datetime type but just string that fit The timestamp may be specified in the format "2012-10-30 18:17:16 in man timedatectl. While later the host.timezone in xapi db will be IANA Timezone format. It's just like the xsconsole's set-timezone and set-time-manually and also most time setting logic in UI.

in
try
Helpers.call_script !Xapi_globs.timedatectl ["set-time"; param] |> ignore ;
()
with e -> Helpers.internal_error "%s" (ExnHelper.string_of_exn e)
5 changes: 5 additions & 0 deletions ocaml/xapi/xapi_host.mli
Original file line number Diff line number Diff line change
Expand Up @@ -627,3 +627,8 @@ val sync_ntp_config : __context:Context.t -> host:API.ref_host -> unit

val get_ntp_servers_status :
__context:Context.t -> self:API.ref_host -> (string * string) list

val get_ntp_synchronized : __context:Context.t -> self:API.ref_host -> bool

val set_server_localtime :
__context:Context.t -> self:API.ref_host -> value:Clock.Date.t -> unit
9 changes: 9 additions & 0 deletions ocaml/xapi/xapi_host_ntp.ml
Original file line number Diff line number Diff line change
Expand Up @@ -175,3 +175,12 @@ let promote_legacy_default_servers () =
set_servers_in_conf defaults ;
restart_ntp_service ()
)

let is_synchronized () =
let patterns = ["System clock synchronized: yes"; "NTP synchronized: yes"] in
try
Helpers.call_script !Xapi_globs.timedatectl ["status"]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another option is to use c function ntp_gettime to get the maxerror of the system. That can avoid the timedatectl output changes in different versions. Although the current method is easy enough too. I'm OK for both.

|> String.split_on_char '\n'
|> List.exists ((Fun.flip List.mem) patterns)
|> Result.ok
with e -> Error (ExnHelper.string_of_exn e)
2 changes: 2 additions & 0 deletions ocaml/xapi/xapi_host_ntp.mli
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,5 @@ val get_servers_from_conf : unit -> string list
val is_ntp_dhcp_enabled : unit -> bool

val get_servers_status : unit -> (string * string) list

val is_synchronized : unit -> (bool, string) Result.t
Loading