From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) by lore.proxmox.com (Postfix) with ESMTPS id 9327F1FF13F for ; Thu, 18 Jun 2026 09:49:32 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id C3A33D1E0; Thu, 18 Jun 2026 09:49:31 +0200 (CEST) From: Maximiliano Sandoval To: pbs-devel@lists.proxmox.com Subject: [PATCH backup 1/2] fix #7175: api: time: use timedatectl instead of /etc/timezone Date: Thu, 18 Jun 2026 09:49:00 +0200 Message-ID: <20260618074926.1464533-1-m.sandoval@proxmox.com> X-Mailer: git-send-email 2.47.3 MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1781768908385 X-SPAM-LEVEL: Spam detection results: 0 AWL -0.024 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment POISEN_SPAM_PILL 0.1 Meta: its spam POISEN_SPAM_PILL_1 0.1 random spam to be learned in bayes POISEN_SPAM_PILL_3 0.1 random spam to be learned in bayes SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record URIBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [time.rs] Message-ID-Hash: DX6PSPOMZK6X5TMHZ2OLHZ7MTNW3TMJB X-Message-ID-Hash: DX6PSPOMZK6X5TMHZ2OLHZ7MTNW3TMJB X-MailFrom: m.sandoval@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox Backup Server development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: The Proxmox Backup Server 4.2 iso does not create /etc/timezone, so we use timedatectl as recommended at [1]. As per timedatectl(1) set-timezone will alter the /etc/localtime symlink, and we return early on set_timezone() if the command succeeds. [1] https://salsa.debian.org/glibc-team/tzdata/-/blob/trixie/debian/README.Debian?ref_type=heads#L54 Signed-off-by: Maximiliano Sandoval --- Tested: - proxmox-backup-debug api get /nodes/{node}/time - proxmox-backup-debug api set /nodes/{node}/time --timezone $tz - Checked that /etc/timezone is not created - Captured dbus output via busctl capture > output.pcapng and checked that the org.freedesktop.timedate1 interface (used by timedatectl) is being used for {g,s}etting the timezone. - Verified that the symlink at /etc/localtime points to the right place. Note that at the moment both getting and setting the timezone work as expected without this patch, in practice the only difference is that /etc/timezone will not be created anymore after setting the timezone. src/api2/node/time.rs | 74 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 63 insertions(+), 11 deletions(-) diff --git a/src/api2/node/time.rs b/src/api2/node/time.rs index 6baca378e..9ab4e84d8 100644 --- a/src/api2/node/time.rs +++ b/src/api2/node/time.rs @@ -1,16 +1,16 @@ +use std::process::Command; + use anyhow::{Error, bail, format_err}; use serde_json::{Value, json}; use proxmox_router::{Permission, Router}; use proxmox_schema::api; -use proxmox_sys::fs::{CreateOptions, file_read_firstline, replace_file}; use pbs_api_types::{NODE_SCHEMA, PRIV_SYS_MODIFY, TIME_ZONE_SCHEMA}; fn read_etc_localtime() -> Result { - // use /etc/timezone - if let Ok(line) = file_read_firstline("/etc/timezone") { - return Ok(line.trim().to_owned()); + if let Ok(timezone) = timedatectl_get_timezone() { + return Ok(timezone); } // otherwise guess from the /etc/localtime symlink @@ -24,6 +24,61 @@ fn read_etc_localtime() -> Result { } } +fn timedatectl_get_timezone() -> Result { + let output = Command::new("timedatectl") + .args(["show", "--property=Timezone", "--value"]) + .output() + .map_err(|err| format_err!("failed to execute timedatectl show - {err}"))?; + + if !output.status.success() { + if let Some(code) = output.status.code() { + let msg = String::from_utf8(output.stderr) + .map(|s| { + if s.is_empty() { + String::from("no error message") + } else { + s + } + }) + .unwrap_or_else(|_| String::from("non utf8 error message (suppressed)")); + bail!("timedatectl show failed with status code {code} - {msg}",); + } else { + bail!("timedatectl terminated by signal",); + } + } + + let timezone = String::from_utf8(output.stdout) + .map_err(|err| format_err!("non utf8 timezone from timedatectl - {err}"))?; + + Ok(timezone) +} + +fn timedatectl_set_timezone(timezone: &str) -> Result<(), Error> { + let output = Command::new("timedatectl") + .args(["set-timezone", timezone]) + .output() + .map_err(|err| format_err!("failed to execute timedatectl set-timezone - {err}"))?; + + if !output.status.success() { + if let Some(code) = output.status.code() { + let msg = String::from_utf8(output.stderr) + .map(|s| { + if s.is_empty() { + String::from("no error message") + } else { + s + } + }) + .unwrap_or_else(|_| String::from("non utf8 error message (suppressed)")); + bail!("timedatectl set-timezone failed with status code {code} - {msg}",); + } else { + bail!("timedatectl terminated by signal",); + } + } + + Ok(()) +} + #[api( input: { properties: { @@ -88,19 +143,16 @@ fn get_time(_param: Value) -> Result { )] /// Set time zone fn set_timezone(timezone: String, _param: Value) -> Result { + if timedatectl_set_timezone(&timezone).is_ok() { + return Ok(Value::Null); + } + let path = std::path::PathBuf::from(format!("/usr/share/zoneinfo/{timezone}")); if !path.exists() { bail!("No such timezone."); } - replace_file( - "/etc/timezone", - timezone.as_bytes(), - CreateOptions::new(), - true, - )?; - let _ = std::fs::remove_file("/etc/localtime"); use std::os::unix::fs::symlink; -- 2.47.3