]> git.proxmox.com Git - proxmox-apt.git/commitdiff
add more functions to check repositories
authorFabian Ebner <f.ebner@proxmox.com>
Wed, 23 Jun 2021 13:38:56 +0000 (15:38 +0200)
committerThomas Lamprecht <t.lamprecht@proxmox.com>
Wed, 23 Jun 2021 14:00:30 +0000 (16:00 +0200)
Currently includes check for suites and check for official URIs

Signed-off-by: Fabian Ebner <f.ebner@proxmox.com>
src/repositories/file.rs
src/repositories/mod.rs
src/repositories/release.rs [new file with mode: 0644]
src/repositories/repository.rs
tests/repositories.rs
tests/sources.list.d.expected/bad.sources [new file with mode: 0644]
tests/sources.list.d.expected/pve.list
tests/sources.list.d/bad.sources [new file with mode: 0644]
tests/sources.list.d/pve.list

index bc48bf2f82baf04ac944ea9cdad1cf7f2cfc2fa6..6225f1c48987127d3032c55d3290c00ddabe570c 100644 (file)
@@ -2,10 +2,13 @@ use std::convert::TryFrom;
 use std::fmt::Display;
 use std::path::{Path, PathBuf};
 
-use anyhow::{format_err, Error};
+use anyhow::{bail, format_err, Error};
 use serde::{Deserialize, Serialize};
 
-use crate::repositories::repository::{APTRepository, APTRepositoryFileType};
+use crate::repositories::release::{get_current_release_codename, DEBIAN_SUITES};
+use crate::repositories::repository::{
+    APTRepository, APTRepositoryFileType, APTRepositoryPackageType,
+};
 
 use proxmox::api::api;
 
@@ -85,6 +88,29 @@ impl std::error::Error for APTRepositoryFileError {
     }
 }
 
+#[api]
+#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
+#[serde(rename_all = "kebab-case")]
+/// Additional information for a repository.
+pub struct APTRepositoryInfo {
+    /// Path to the defining file.
+    #[serde(skip_serializing_if = "String::is_empty")]
+    pub path: String,
+
+    /// Index of the associated respository within the file (starting from 0).
+    pub index: usize,
+
+    /// The property from which the info originates (e.g. "Suites")
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub property: Option<String>,
+
+    /// Info kind (e.g. "warning")
+    pub kind: String,
+
+    /// Info message
+    pub message: String,
+}
+
 impl APTRepositoryFile {
     /// Creates a new `APTRepositoryFile` without parsing.
     ///
@@ -271,4 +297,93 @@ impl APTRepositoryFile {
 
         Ok(())
     }
+
+    /// Checks if old or unstable suites are configured and also that the
+    /// `stable` keyword is not used.
+    pub fn check_suites(&self) -> Result<Vec<APTRepositoryInfo>, Error> {
+        let mut infos = vec![];
+
+        for (n, repo) in self.repositories.iter().enumerate() {
+            if !repo
+                .types
+                .iter()
+                .any(|package_type| *package_type == APTRepositoryPackageType::Deb)
+            {
+                continue;
+            }
+
+            let mut add_info = |kind, message| {
+                infos.push(APTRepositoryInfo {
+                    path: self.path.clone(),
+                    index: n,
+                    property: Some("Suites".to_string()),
+                    kind,
+                    message,
+                })
+            };
+
+            let current_suite = get_current_release_codename()?;
+
+            let current_index = match DEBIAN_SUITES
+                .iter()
+                .position(|&suite| suite == current_suite)
+            {
+                Some(index) => index,
+                None => bail!("unknown release {}", current_suite),
+            };
+
+            for (n, suite) in DEBIAN_SUITES.iter().enumerate() {
+                if repo.has_suite_variant(suite) {
+                    if n < current_index {
+                        add_info(
+                            "warning".to_string(),
+                            format!("old suite '{}' configured!", suite),
+                        );
+                    }
+
+                    if n == current_index + 1 {
+                        add_info(
+                            "ignore-pre-upgrade-warning".to_string(),
+                            format!("suite '{}' should not be used in production!", suite),
+                        );
+                    }
+
+                    if n > current_index + 1 {
+                        add_info(
+                            "warning".to_string(),
+                            format!("suite '{}' should not be used in production!", suite),
+                        );
+                    }
+                }
+            }
+
+            if repo.has_suite_variant("stable") {
+                add_info(
+                    "warning".to_string(),
+                    "use the name of the stable distribution instead of 'stable'!".to_string(),
+                );
+            }
+        }
+
+        Ok(infos)
+    }
+
+    /// Checks for official URIs.
+    pub fn check_uris(&self) -> Vec<APTRepositoryInfo> {
+        let mut infos = vec![];
+
+        for (n, repo) in self.repositories.iter().enumerate() {
+            if repo.has_official_uri() {
+                infos.push(APTRepositoryInfo {
+                    path: self.path.clone(),
+                    index: n,
+                    kind: "badge".to_string(),
+                    property: Some("URIs".to_string()),
+                    message: "official host name".to_string(),
+                });
+            }
+        }
+
+        infos
+    }
 }
index d540bcb25cd580cc0a3ab68dff032d29191c35b8..fc54857e51a7d17e0b74da0ebd2471e355f017b7 100644 (file)
@@ -9,7 +9,9 @@ pub use repository::{
 };
 
 mod file;
-pub use file::{APTRepositoryFile, APTRepositoryFileError};
+pub use file::{APTRepositoryFile, APTRepositoryFileError, APTRepositoryInfo};
+
+mod release;
 
 const APT_SOURCES_LIST_FILENAME: &str = "/etc/apt/sources.list";
 const APT_SOURCES_LIST_DIRECTORY: &str = "/etc/apt/sources.list.d/";
@@ -37,6 +39,23 @@ fn common_digest(files: &[APTRepositoryFile]) -> [u8; 32] {
     openssl::sha::sha256(&common_raw[..])
 }
 
+/// Provides additional information about the repositories.
+///
+/// The kind of information can be:
+/// `warnings` for bad suites.
+/// `ignore-pre-upgrade-warning` when the next stable suite is configured.
+/// `badge` for official URIs.
+pub fn check_repositories(files: &[APTRepositoryFile]) -> Result<Vec<APTRepositoryInfo>, Error> {
+    let mut infos = vec![];
+
+    for file in files.iter() {
+        infos.append(&mut file.check_suites()?);
+        infos.append(&mut file.check_uris());
+    }
+
+    Ok(infos)
+}
+
 /// Returns all APT repositories configured in `/etc/apt/sources.list` and
 /// in `/etc/apt/sources.list.d` including disabled repositories.
 ///
diff --git a/src/repositories/release.rs b/src/repositories/release.rs
new file mode 100644 (file)
index 0000000..688f038
--- /dev/null
@@ -0,0 +1,42 @@
+use anyhow::{bail, format_err, Error};
+
+use std::io::{BufRead, BufReader};
+
+/// The suites of Debian releases, ordered chronologically, with variable releases
+/// like 'oldstable' and 'testing' ordered at the extremes. Does not include 'stable'.
+pub const DEBIAN_SUITES: [&str; 15] = [
+    "oldoldstable",
+    "oldstable",
+    "lenny",
+    "squeeze",
+    "wheezy",
+    "jessie",
+    "stretch",
+    "buster",
+    "bullseye",
+    "bookworm",
+    "trixie",
+    "sid",
+    "testing",
+    "unstable",
+    "experimental",
+];
+
+/// Read the `VERSION_CODENAME` from `/etc/os-release`.
+pub fn get_current_release_codename() -> Result<String, Error> {
+    let raw = std::fs::read("/etc/os-release")
+        .map_err(|err| format_err!("unable to read '/etc/os-release' - {}", err))?;
+
+    let reader = BufReader::new(&*raw);
+
+    for line in reader.lines() {
+        let line = line.map_err(|err| format_err!("unable to read '/etc/os-release' - {}", err))?;
+
+        if let Some(codename) = line.strip_prefix("VERSION_CODENAME=") {
+            let codename = codename.trim_matches(&['"', '\''][..]);
+            return Ok(codename.to_string());
+        }
+    }
+
+    bail!("unable to parse codename from '/etc/os-release'");
+}
index 9ee7df99829bc9ea795b07df541907751fbf6dec..875e4ee555c09613a9344071c768470cf46add32 100644 (file)
@@ -266,6 +266,30 @@ impl APTRepository {
         Ok(())
     }
 
+    /// Check if a variant of the given suite is configured in this repository
+    pub fn has_suite_variant(&self, base_suite: &str) -> bool {
+        self.suites
+            .iter()
+            .any(|suite| suite_variant(suite).0 == base_suite)
+    }
+
+    /// Checks if an official host is configured in the repository.
+    pub fn has_official_uri(&self) -> bool {
+        for uri in self.uris.iter() {
+            if let Some(host) = host_from_uri(uri) {
+                if host == "proxmox.com"
+                    || host.ends_with(".proxmox.com")
+                    || host == "debian.org"
+                    || host.ends_with(".debian.org")
+                {
+                    return true;
+                }
+            }
+        }
+
+        false
+    }
+
     /// Writes a repository in the corresponding format followed by a blank.
     ///
     /// Expects that `basic_check()` for the repository was successful.
@@ -277,6 +301,40 @@ impl APTRepository {
     }
 }
 
+/// Get the host part from a given URI.
+fn host_from_uri(uri: &str) -> Option<&str> {
+    let host = uri.strip_prefix("http")?;
+    let host = host.strip_prefix("s").unwrap_or(host);
+    let mut host = host.strip_prefix("://")?;
+
+    if let Some(end) = host.find('/') {
+        host = &host[..end];
+    }
+
+    if let Some(begin) = host.find('@') {
+        host = &host[(begin + 1)..];
+    }
+
+    if let Some(end) = host.find(':') {
+        host = &host[..end];
+    }
+
+    Some(host)
+}
+
+/// Splits the suite into its base part and variant.
+fn suite_variant(suite: &str) -> (&str, &str) {
+    let variants = ["-backports-sloppy", "-backports", "-updates", "/updates"];
+
+    for variant in variants.iter() {
+        if let Some(base) = suite.strip_suffix(variant) {
+            return (base, variant);
+        }
+    }
+
+    (suite, "")
+}
+
 /// Writes a repository in one-line format followed by a blank line.
 ///
 /// Expects that `repo.file_type == APTRepositoryFileType::List`.
index 477d7184aec79fee0b0be7dda8a6bc62f022b297..58f13228a3a393d9f2845c82f4ac597ec13bc86d 100644 (file)
@@ -2,7 +2,7 @@ use std::path::PathBuf;
 
 use anyhow::{bail, format_err, Error};
 
-use proxmox_apt::repositories::APTRepositoryFile;
+use proxmox_apt::repositories::{check_repositories, APTRepositoryFile, APTRepositoryInfo};
 
 #[test]
 fn test_parse_write() -> Result<(), Error> {
@@ -160,3 +160,107 @@ fn test_empty_write() -> Result<(), Error> {
 
     Ok(())
 }
+
+#[test]
+fn test_check_repositories() -> Result<(), Error> {
+    let test_dir = std::env::current_dir()?.join("tests");
+    let read_dir = test_dir.join("sources.list.d");
+
+    let absolute_suite_list = read_dir.join("absolute_suite.list");
+    let mut file = APTRepositoryFile::new(&absolute_suite_list)?.unwrap();
+    file.parse()?;
+
+    let infos = check_repositories(&vec![file])?;
+
+    assert_eq!(infos.is_empty(), true);
+    let pve_list = read_dir.join("pve.list");
+    let mut file = APTRepositoryFile::new(&pve_list)?.unwrap();
+    file.parse()?;
+
+    let path_string = pve_list.into_os_string().into_string().unwrap();
+
+    let mut expected_infos = vec![];
+    for n in 0..=5 {
+        expected_infos.push(APTRepositoryInfo {
+            path: path_string.clone(),
+            index: n,
+            property: Some("URIs".to_string()),
+            kind: "badge".to_string(),
+            message: "official host name".to_string(),
+        });
+    }
+    expected_infos.sort();
+
+    let mut infos = check_repositories(&vec![file])?;
+    infos.sort();
+
+    assert_eq!(infos, expected_infos);
+
+    let bad_sources = read_dir.join("bad.sources");
+    let mut file = APTRepositoryFile::new(&bad_sources)?.unwrap();
+    file.parse()?;
+
+    let path_string = bad_sources.into_os_string().into_string().unwrap();
+
+    let mut expected_infos = vec![
+        APTRepositoryInfo {
+            path: path_string.clone(),
+            index: 0,
+            property: Some("Suites".to_string()),
+            kind: "warning".to_string(),
+            message: "suite 'sid' should not be used in production!".to_string(),
+        },
+        APTRepositoryInfo {
+            path: path_string.clone(),
+            index: 1,
+            property: Some("Suites".to_string()),
+            kind: "warning".to_string(),
+            message: "old suite 'lenny' configured!".to_string(),
+        },
+        APTRepositoryInfo {
+            path: path_string.clone(),
+            index: 2,
+            property: Some("Suites".to_string()),
+            kind: "warning".to_string(),
+            message: "old suite 'stretch' configured!".to_string(),
+        },
+        APTRepositoryInfo {
+            path: path_string.clone(),
+            index: 3,
+            property: Some("Suites".to_string()),
+            kind: "warning".to_string(),
+            message: "use the name of the stable distribution instead of 'stable'!".to_string(),
+        },
+        APTRepositoryInfo {
+            path: path_string.clone(),
+            index: 4,
+            property: Some("Suites".to_string()),
+            kind: "ignore-pre-upgrade-warning".to_string(),
+            message: "suite 'bookworm' should not be used in production!".to_string(),
+        },
+        APTRepositoryInfo {
+            path: path_string.clone(),
+            index: 5,
+            property: Some("Suites".to_string()),
+            kind: "warning".to_string(),
+            message: "suite 'testing' should not be used in production!".to_string(),
+        },
+    ];
+    for n in 0..=5 {
+        expected_infos.push(APTRepositoryInfo {
+            path: path_string.clone(),
+            index: n,
+            property: Some("URIs".to_string()),
+            kind: "badge".to_string(),
+            message: "official host name".to_string(),
+        });
+    }
+    expected_infos.sort();
+
+    let mut infos = check_repositories(&vec![file])?;
+    infos.sort();
+
+    assert_eq!(infos, expected_infos);
+
+    Ok(())
+}
diff --git a/tests/sources.list.d.expected/bad.sources b/tests/sources.list.d.expected/bad.sources
new file mode 100644 (file)
index 0000000..7eff6a5
--- /dev/null
@@ -0,0 +1,30 @@
+Types: deb
+URIs: http://ftp.at.debian.org/debian
+Suites: sid
+Components: main contrib
+
+Types: deb
+URIs: http://ftp.at.debian.org/debian
+Suites: lenny-backports
+Components: contrib
+
+Types: deb
+URIs: http://security.debian.org:80
+Suites: stretch/updates
+Components: main contrib
+
+Types: deb
+URIs: http://ftp.at.debian.org:80/debian
+Suites: stable
+Components: main
+
+Types: deb
+URIs: http://ftp.at.debian.org/debian
+Suites: bookworm
+Components: main
+
+Types: deb
+URIs: http://ftp.at.debian.org/debian
+Suites: testing
+Components: main
+
index 95725ab0b7a2de612057d289f1ff90169927aabf..c8012618749cacce29f4610063d3a35da9f66d6a 100644 (file)
@@ -8,6 +8,8 @@ deb http://download.proxmox.com/debian/pve bullseye pve-no-subscription
 
 # deb https://enterprise.proxmox.com/debian/pve bullseye pve-enterprise
 
+deb-src https://enterprise.proxmox.com/debian/pve buster pve-enterprise
+
 # security updates
 deb http://security.debian.org/debian-security bullseye-security main contrib
 
diff --git a/tests/sources.list.d/bad.sources b/tests/sources.list.d/bad.sources
new file mode 100644 (file)
index 0000000..46eb82a
--- /dev/null
@@ -0,0 +1,29 @@
+Types: deb
+URIs: http://ftp.at.debian.org/debian
+Suites: sid
+Components: main contrib
+
+Types: deb
+URIs: http://ftp.at.debian.org/debian
+Suites: lenny-backports
+Components: contrib
+
+Types: deb
+URIs: http://security.debian.org:80
+Suites: stretch/updates
+Components: main contrib
+
+Suites: stable
+URIs: http://ftp.at.debian.org:80/debian
+Components: main
+Types: deb
+
+Suites: bookworm
+URIs: http://ftp.at.debian.org/debian
+Components: main
+Types: deb
+
+Suites: testing
+URIs: http://ftp.at.debian.org/debian
+Components: main
+Types: deb
index c6d451a754ccafd181d417d7224966966f5761a5..4d36d3db54a3db49ddf613c149cfab56056834e0 100644 (file)
@@ -6,5 +6,7 @@ deb http://ftp.debian.org/debian bullseye-updates main contrib
 deb http://download.proxmox.com/debian/pve bullseye pve-no-subscription
 # deb https://enterprise.proxmox.com/debian/pve bullseye pve-enterprise
 
+deb-src https://enterprise.proxmox.com/debian/pve buster pve-enterprise
+
 # security updates
 deb http://security.debian.org/debian-security bullseye-security main contrib