diff --git a/ocaml/idl/datamodel_host.ml b/ocaml/idl/datamodel_host.ml index 3d3c4d8e26e..952ab7db791 100644 --- a/ocaml/idl/datamodel_host.ml +++ b/ocaml/idl/datamodel_host.ml @@ -2627,6 +2627,25 @@ let get_ntp_servers_status = ) ~allowed_roles:_R_READ_ONLY () +let set_timezone = + call ~name:"set_timezone" ~lifecycle:[] ~doc:"Set the host's timezone." + ~params: + [ + (Ref _host, "self", "The host") + ; ( String + , "value" + , "The time zone identifier as defined in the IANA Time Zone Database" + ) + ] + ~allowed_roles:_R_POOL_OP () + +let list_timezones = + call ~name:"list_timezones" ~lifecycle:[] + ~doc:"List all available timezones on the host." + ~params:[(Ref _host, "self", "The host")] + ~result:(Set String, "The set of available timezones on the host") + ~allowed_roles:_R_READ_ONLY () + (** Hosts *) let t = create_obj ~in_db:true @@ -2779,6 +2798,8 @@ let t = ; disable_ntp ; enable_ntp ; get_ntp_servers_status + ; set_timezone + ; list_timezones ] ~contents: ([ @@ -3256,6 +3277,9 @@ let t = ; field ~qualifier:DynamicRO ~lifecycle:[] ~ty:Bool ~default_value:(Some (VBool false)) "ntp_enabled" "Reflects whether NTP is enabled on the host" + ; field ~qualifier:DynamicRO ~lifecycle:[] ~ty:String + ~default_value:(Some (VString "UTC")) "timezone" + "The time zone identifier as defined in the IANA Time Zone Database" ] ) () diff --git a/ocaml/idl/schematest.ml b/ocaml/idl/schematest.ml index 71a8a2c5ccf..78af76524a2 100644 --- a/ocaml/idl/schematest.ml +++ b/ocaml/idl/schematest.ml @@ -3,7 +3,7 @@ let hash x = Digest.string x |> Digest.to_hex (* BEWARE: if this changes, check that schema has been bumped accordingly in ocaml/idl/datamodel_common.ml, usually schema_minor_vsn *) -let last_known_schema_hash = "34c69ac52c1e6c1d46bc35f610562a58" +let last_known_schema_hash = "c72edb27945bceb074de3fa54381ddd4" let current_schema_hash : string = let open Datamodel_types in diff --git a/ocaml/tests/common/test_common.ml b/ocaml/tests/common/test_common.ml index 8a96bcc8571..c71f57b72d7 100644 --- a/ocaml/tests/common/test_common.ml +++ b/ocaml/tests/common/test_common.ml @@ -225,7 +225,7 @@ let make_host2 ~__context ?(ref = Ref.make ()) ?(uuid = make_uuid ()) ~last_update_hash:"" ~ssh_enabled:true ~ssh_enabled_timeout:0L ~ssh_expiry:Date.epoch ~console_idle_timeout:0L ~ssh_auto_mode:false ~max_cstate:"" ~secure_boot:false ~ntp_mode:`ntp_mode_dhcp - ~ntp_custom_servers:[] ~ntp_enabled:false ; + ~ntp_custom_servers:[] ~ntp_enabled:false ~timezone:"UTC" ; ref let make_pif ~__context ~network ~host ?(device = "eth0") diff --git a/ocaml/xapi-cli-server/records.ml b/ocaml/xapi-cli-server/records.ml index 7d02c689c06..1ec8705f34d 100644 --- a/ocaml/xapi-cli-server/records.ml +++ b/ocaml/xapi-cli-server/records.ml @@ -3426,6 +3426,12 @@ let host_record rpc session_id host = ; make_field ~name:"ntp_enabled" ~get:(fun () -> string_of_bool (x ()).API.host_ntp_enabled) () + ; make_field ~name:"timezone" + ~get:(fun () -> (x ()).API.host_timezone) + ~set:(fun value -> + Client.Host.set_timezone ~rpc ~session_id ~self:host ~value + ) + () ] } diff --git a/ocaml/xapi/dbsync_slave.ml b/ocaml/xapi/dbsync_slave.ml index d9133b1a150..8ca590a1cc6 100644 --- a/ocaml/xapi/dbsync_slave.ml +++ b/ocaml/xapi/dbsync_slave.ml @@ -418,6 +418,18 @@ let update_env __context sync_keys = switched_sync Xapi_globs.sync_ntp_config (fun () -> Xapi_host.sync_ntp_config ~__context ~host:localhost ) ; + switched_sync Xapi_globs.sync_timezone (fun () -> + let timezone = + try + let linkpath = Unix.realpath "/etc/localtime" in + Scanf.sscanf linkpath "/usr/share/zoneinfo/%s" (fun tz -> tz) + with e -> + warn "%s error when sync timezone: %s" __FUNCTION__ + (Printexc.to_string e) ; + "UTC" + in + Db.Host.set_timezone ~__context ~self:localhost ~value:timezone + ) ; switched_sync Xapi_globs.sync_secure_boot (fun () -> let result = diff --git a/ocaml/xapi/message_forwarding.ml b/ocaml/xapi/message_forwarding.ml index d3cf4d9ae8c..3518843680b 100644 --- a/ocaml/xapi/message_forwarding.ml +++ b/ocaml/xapi/message_forwarding.ml @@ -4170,6 +4170,20 @@ 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 set_timezone ~__context ~self ~value = + info "Host.set_timezone: host = '%s'; value = '%s'" + (host_uuid ~__context self) + value ; + let local_fn = Local.Host.set_timezone ~self ~value in + let remote_fn = Client.Host.set_timezone ~self ~value in + do_op_on ~local_fn ~__context ~host:self ~remote_fn + + let list_timezones ~__context ~self = + info "Host.list_timezones: host = '%s'" (host_uuid ~__context self) ; + let local_fn = Local.Host.list_timezones ~self in + let remote_fn = Client.Host.list_timezones ~self in + do_op_on ~local_fn ~__context ~host:self ~remote_fn end module Host_crashdump = struct diff --git a/ocaml/xapi/xapi_globs.ml b/ocaml/xapi/xapi_globs.ml index 91d996bce56..a9021154bd0 100644 --- a/ocaml/xapi/xapi_globs.ml +++ b/ocaml/xapi/xapi_globs.ml @@ -383,6 +383,8 @@ let sync_max_cstate = "sync_max_cstate" let sync_ntp_config = "sync_ntp_config" +let sync_timezone = "sync_timezone" + let sync_secure_boot = "sync_secure_boot" let sync_pci_devices = "sync_pci_devices" @@ -812,6 +814,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" @@ -1907,6 +1911,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) diff --git a/ocaml/xapi/xapi_host.ml b/ocaml/xapi/xapi_host.ml index 1d9425f89f1..2cf15046a98 100644 --- a/ocaml/xapi/xapi_host.ml +++ b/ocaml/xapi/xapi_host.ml @@ -1095,7 +1095,7 @@ let create ~__context ~uuid ~name_label ~name_description:_ ~hostname ~address ~pending_guidances_recommended:[] ~pending_guidances_full:[] ~ssh_enabled ~ssh_enabled_timeout ~ssh_expiry ~console_idle_timeout ~ssh_auto_mode ~max_cstate:"" ~secure_boot ~ntp_mode:`ntp_mode_dhcp ~ntp_custom_servers:[] - ~ntp_enabled:false ; + ~ntp_enabled:false ~timezone:"UTC" ; (* If the host we're creating is us, make sure its set to live *) Db.Host_metrics.set_last_updated ~__context ~self:metrics ~value:(Date.now ()) ; Db.Host_metrics.set_live ~__context ~self:metrics ~value:host_is_us ; @@ -3561,3 +3561,23 @@ let get_ntp_servers_status ~__context ~self:_ = Xapi_host_ntp.get_servers_status () else [] + +let set_timezone ~__context ~self ~value = + try + let _ = + Helpers.call_script !Xapi_globs.timedatectl ["set-timezone"; value] + in + Db.Host.set_timezone ~__context ~self ~value + with + | Forkhelpers.Spawn_internal_error (stderr, _, _) + when String.starts_with ~prefix:"Failed to set time zone: Invalid" stderr -> + raise + (Api_errors.Server_error (Api_errors.invalid_value, ["timezone"; value])) + | e -> + Helpers.internal_error "%s" (ExnHelper.string_of_exn e) + +let list_timezones ~__context ~self:_ = + try + Helpers.call_script !Xapi_globs.timedatectl ["list-timezones"] + |> Astring.String.cuts ~empty:false ~sep:"\n" + with e -> Helpers.internal_error "%s" (ExnHelper.string_of_exn e) diff --git a/ocaml/xapi/xapi_host.mli b/ocaml/xapi/xapi_host.mli index bdf889d1ab5..942026ed52d 100644 --- a/ocaml/xapi/xapi_host.mli +++ b/ocaml/xapi/xapi_host.mli @@ -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 set_timezone : + __context:Context.t -> self:API.ref_host -> value:string -> unit + +val list_timezones : __context:Context.t -> self:API.ref_host -> string list