From d854bdec0aa318fb1db33c96a7f2e52976452bc4 Mon Sep 17 00:00:00 2001
From: zhangyao2022 <zhangyao108@huawei.com>
Date: Tue, 20 Jun 2023 17:26:52 +0800
Subject: [PATCH] feature: support hostname setup

---
 Cargo.toml                               |   1 +
 exts/hostname_setup/Cargo.toml           |  11 ++
 exts/hostname_setup/src/main.rs          | 235 +++++++++++++++++++++++
 install.sh                               |   1 +
 libs/basic/src/lib.rs                    |   1 +
 libs/basic/src/os_release.rs             | 109 +++++++++++
 9 files changed, 368 insertions(+), 1 deletion(-)
 create mode 100644 exts/hostname_setup/Cargo.toml
 create mode 100644 exts/hostname_setup/src/main.rs
 create mode 100644 libs/basic/src/os_release.rs

diff --git a/Cargo.toml b/Cargo.toml
index b96ec26..a375070 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -120,6 +120,7 @@ members = [
   # external binaries
   "exts/init",
   "exts/sctl",
+  "exts/hostname_setup",
   #internal libraries crates
   "libs/cmdproto",
   #external libraries crates
diff --git a/exts/hostname_setup/Cargo.toml b/exts/hostname_setup/Cargo.toml
new file mode 100644
index 0000000..b2cbdcf
--- /dev/null
+++ b/exts/hostname_setup/Cargo.toml
@@ -0,0 +1,11 @@
+[package]
+name = "hostname_setup"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+nix = "0.24"
+log = "0.4"
+libc = "0.2.*"
+constants = { version = "0.1.0", path = "../../libs/constants" }
+basic = { path = "../../libs/basic" }
diff --git a/exts/hostname_setup/src/main.rs b/exts/hostname_setup/src/main.rs
new file mode 100644
index 0000000..4dc5a1b
--- /dev/null
+++ b/exts/hostname_setup/src/main.rs
@@ -0,0 +1,235 @@
+// Copyright (c) 2022 Huawei Technologies Co.,Ltd. All rights reserved.
+//
+// sysMaster is licensed under Mulan PSL v2.
+// You can use this software according to the terms and conditions of the Mulan
+// PSL v2.
+// You may obtain a copy of Mulan PSL v2 at:
+//         http://license.coscl.org.cn/MulanPSL2
+// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY
+// KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
+// NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
+// See the Mulan PSL v2 for more details.
+
+//! hostname setup
+
+use basic::logger;
+use basic::os_release;
+use basic::proc_cmdline;
+use nix::Result;
+use std::fmt;
+use std::fmt::Display;
+
+const SYSTEMD_HOSTNAME_KEY: &str = "systemd.hostname";
+const SYSMASTER_HOSTNAME_KEY: &str = "sysmaster.hostname";
+const DEFAULT_HOSTNAME: &str = "localhost";
+
+#[derive(Debug)]
+struct Hostname {
+    hostname: String,
+}
+
+impl Hostname {
+    fn from_string(str_hostname: &str) -> Option<Hostname> {
+        let hostname = Hostname::new(str_hostname.trim());
+        if hostname.valid() {
+            Some(hostname)
+        } else {
+            None
+        }
+    }
+
+    fn from_cmdline() -> Option<Hostname> {
+        let systemd_hostname = match proc_cmdline::cmdline_get_value(SYSTEMD_HOSTNAME_KEY) {
+            Err(e) => {
+                log::warn!(
+                    "Failed to get proc cmdline by key {}: {}",
+                    SYSTEMD_HOSTNAME_KEY,
+                    e
+                );
+                None
+            }
+            Ok(hostname) => hostname,
+        };
+
+        systemd_hostname.map_or_else(
+            || match proc_cmdline::cmdline_get_value(SYSMASTER_HOSTNAME_KEY) {
+                Err(e) => {
+                    log::warn!(
+                        "Failed to get proc cmdline by key {}: {}",
+                        SYSMASTER_HOSTNAME_KEY,
+                        e
+                    );
+                    None
+                }
+                Ok(hostname) => hostname.and_then(|hostname| Hostname::from_string(&hostname)),
+            },
+            |hostname| Hostname::from_string(&hostname),
+        )
+    }
+
+    fn from_etc_hostname() -> Option<Self> {
+        match std::fs::read_to_string("/etc/hostname") {
+            Err(e) => {
+                log::warn!("Failed to get /etc/hostname: {}", e);
+                None
+            }
+            Ok(hostname) => Hostname::from_string(&hostname),
+        }
+    }
+
+    fn new(hostname: &str) -> Self {
+        Hostname {
+            hostname: hostname.trim().to_string(),
+        }
+    }
+
+    fn valid(&self) -> bool {
+        if self.hostname.is_empty() {
+            return false;
+        }
+
+        if self.hostname.len() > 64 {
+            return false;
+        }
+
+        let mut dot = true;
+        let mut hyphen = true;
+
+        for c in self.hostname.chars() {
+            if c == '.' {
+                if dot || hyphen {
+                    return false;
+                }
+                dot = true;
+                hyphen = false;
+            } else if c == '-' {
+                if dot {
+                    return false;
+                }
+                dot = false;
+                hyphen = true;
+            } else {
+                if !c.is_ascii_alphanumeric() {
+                    return false;
+                }
+                dot = false;
+                hyphen = false;
+            }
+        }
+
+        if dot {
+            return false;
+        }
+
+        if hyphen {
+            return false;
+        }
+        true
+    }
+
+    fn setup(hostname: &Self) -> Result<()> {
+        if !hostname.valid() {
+            return Err(nix::errno::Errno::EINVAL);
+        }
+
+        let local_hostname = Hostname::local_hostname()?;
+        if local_hostname.eq(hostname) {
+            log::info!("Hostname has already been set: {hostname}.");
+            return Ok(());
+        }
+        nix::unistd::sethostname(&hostname.hostname)
+    }
+
+    fn local_hostname() -> Result<Hostname> {
+        match nix::sys::utsname::uname() {
+            Err(e) => {
+                log::warn!("Failed to get uname: {}", e);
+                Err(e)
+            }
+            Ok(uts_name) => Ok(uts_name
+                .nodename()
+                .to_str()
+                .map_or(Hostname::new(""), Hostname::new)),
+        }
+    }
+
+    fn hostname_is_set() -> bool {
+        Hostname::local_hostname().map_or(false, |hostname| hostname != Hostname::new("(none)"))
+    }
+}
+
+impl PartialEq for Hostname {
+    fn eq(&self, other: &Self) -> bool {
+        self.hostname.eq(&other.hostname)
+    }
+}
+
+impl Display for Hostname {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}", self.hostname)
+    }
+}
+
+impl Default for Hostname {
+    fn default() -> Self {
+        if let Ok(Some(str)) = os_release::get_os_release("DEFAULT_HOSTNAME") {
+            return Hostname::from_string(&str).unwrap_or_else(|| Hostname::new(DEFAULT_HOSTNAME));
+        }
+        Hostname::new(DEFAULT_HOSTNAME)
+    }
+}
+
+fn main() {
+    logger::init_log_to_console("hostname-setup", log::LevelFilter::Info);
+    let mut op_hostname = Hostname::from_cmdline();
+    if op_hostname.is_none() {
+        op_hostname = Hostname::from_etc_hostname();
+    }
+
+    if op_hostname.is_none() && Hostname::hostname_is_set() {
+        log::info!("Hostname has already been set, skipping.");
+        return;
+    }
+
+    let hostname = op_hostname.unwrap_or_default();
+
+    log::info!("Hostname set to: {}.", hostname);
+    if let Err(e) = Hostname::setup(&hostname) {
+        log::error!("Failed to set hostname: {}.", e);
+        std::process::exit(e as i32);
+    }
+    std::process::exit(0);
+}
+
+#[cfg(test)]
+mod test {
+    use crate::Hostname;
+
+    #[test]
+    fn test_hostname_valid() {
+        assert!(Hostname::new("foobar").valid());
+        assert!(Hostname::new("foobar.com").valid());
+        assert!(!Hostname::new("foobar.com.").valid());
+        assert!(Hostname::new("fooBAR").valid());
+        assert!(Hostname::new("fooBAR.com").valid());
+        assert!(!Hostname::new("fooBAR.").valid());
+        assert!(!Hostname::new("fooBAR.com.").valid());
+        assert!(!Hostname::new("fööbar").valid());
+        assert!(!Hostname::new("").valid());
+        assert!(!Hostname::new(".").valid());
+        assert!(!Hostname::new("..").valid());
+        assert!(!Hostname::new("foobar.").valid());
+        assert!(!Hostname::new(".foobar").valid());
+        assert!(!Hostname::new("foo..bar").valid());
+        assert!(!Hostname::new("foo.bar..").valid());
+        assert!(!Hostname::new("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx").valid());
+        assert!(!Hostname::new(
+            "au-xph5-rvgrdsb5hcxc-47et3a5vvkrc-server-wyoz4elpdpe3.openstack.local"
+        )
+        .valid());
+        assert!(Hostname::new("local--host.localdomain").valid());
+        assert!(!Hostname::new("localhost-.localdomain").valid());
+        assert!(!Hostname::new("localhost.-localdomain").valid());
+        assert!(Hostname::new("localhost.localdomain").valid());
+    }
+}
diff --git a/install.sh b/install.sh
index eb1b43e..675c543 100644
--- a/install.sh
+++ b/install.sh
@@ -14,6 +14,7 @@ install -Dm0755 -t ${install_dir} ${target_dir}/fstab || exit 1
 install -Dm0755 -t ${install_dir} ${target_dir}/sysmonitor || exit 1
 install -Dm0755 -t ${install_dir} ${target_dir}/random_seed || exit 1
 install -Dm0755 -t ${install_dir} ${target_dir}/rc-local-generator || exit 1
+install -Dm0755 -t ${install_dir} ${target_dir}/hostname_setup || exit 1
 
 strip ${target_dir}/lib*.so
 
diff --git a/libs/basic/src/lib.rs b/libs/basic/src/lib.rs
index 8678b58..db80efb 100644
--- a/libs/basic/src/lib.rs
+++ b/libs/basic/src/lib.rs
@@ -44,3 +44,4 @@ pub mod user_group_util;
 pub mod uuid;
 pub mod virtualize;
 pub use error::*;
+pub mod os_release;
diff --git a/libs/basic/src/os_release.rs b/libs/basic/src/os_release.rs
new file mode 100644
index 0000000..26e291d
--- /dev/null
+++ b/libs/basic/src/os_release.rs
@@ -0,0 +1,109 @@
+// Copyright (c) 2022 Huawei Technologies Co.,Ltd. All rights reserved.
+//
+// sysMaster is licensed under Mulan PSL v2.
+// You can use this software according to the terms and conditions of the Mulan
+// PSL v2.
+// You may obtain a copy of Mulan PSL v2 at:
+//         http://license.coscl.org.cn/MulanPSL2
+// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY
+// KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
+// NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
+// See the Mulan PSL v2 for more details.
+
+//! Parse info about /etc/os-release
+use std::{
+    fs::File,
+    io::{BufRead, BufReader},
+};
+
+use crate::error::*;
+
+/// get value by key in /etc/os-release
+pub fn get_os_release(key: &str) -> Result<Option<String>> {
+    let reader = BufReader::new(File::open("/etc/os-release").context(IoSnafu)?);
+    for line in reader.lines().map(std::result::Result::unwrap_or_default) {
+        match parse_line(key, &line) {
+            Some(value) => return Ok(Some(value.to_string())),
+            None => continue,
+        }
+    }
+    Ok(None)
+}
+
+fn parse_line<'a>(key: &str, line: &'a str) -> Option<&'a str> {
+    if let Some(pos) = line.chars().position(|c| c == '=') {
+        let line_key = line[..pos].trim();
+        if key == line_key {
+            let s = trim_quotes(line[pos + 1..].trim());
+            if !s.is_empty() {
+                return Some(s);
+            }
+        }
+        None
+    } else {
+        None
+    }
+}
+
+fn trim_quotes(str: &str) -> &str {
+    if ["\"", "'"]
+        .iter()
+        .any(|s| str.starts_with(s) && str.ends_with(s))
+    {
+        str[1..str.len() - 1].trim()
+    } else {
+        str.trim()
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+
+    #[test]
+    fn test_trim_quotes() {
+        let str1 = "\"aaaaa\"";
+        assert_eq!(&str1[1..str1.len() - 1], trim_quotes(str1));
+
+        let str2 = "'aaaaa'";
+        assert_eq!(&str2[1..str2.len() - 1], trim_quotes(str2));
+
+        let str3 = "\"aaaaa";
+        assert_eq!(str3, trim_quotes(str3));
+
+        let str4 = "'aaaaa";
+        assert_eq!(str4, trim_quotes(str4));
+    }
+
+    #[test]
+    fn test_parse_line() {
+        let line = "ID=\"openEuler\"";
+        assert_eq!(parse_line("ID", line), Some("openEuler"));
+        assert_eq!(parse_line("NONE", line), None);
+
+        let line = "ID='openEuler'";
+        assert_eq!(parse_line("ID", line), Some("openEuler"));
+        assert_eq!(parse_line("NONE", line), None);
+
+        let line = "IDopenEuler";
+        assert_eq!(parse_line("ID", line), None);
+
+        let line = "ID=";
+        assert_eq!(parse_line("ID", line), None);
+
+        let line = "ID=\"openEuler   \"";
+        assert_eq!(parse_line("ID", line), Some("openEuler"));
+
+        let line = "ID=\"openEuler   \"";
+        assert_eq!(parse_line("ID", line), Some("openEuler"));
+
+        let line = "ID=\"openEuler\"   ";
+        assert_eq!(parse_line("ID", line), Some("openEuler"));
+
+        let line = "ID=openEuler";
+        assert_eq!(parse_line("ID", line), Some("openEuler"));
+
+        let line = "ID=openEuler  ";
+        assert_eq!(parse_line("ID", line), Some("openEuler"));
+    }
+}
-- 
2.33.0

