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;
}
}
+#[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.
///
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
+ }
}
};
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/";
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.
///
--- /dev/null
+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'");
+}
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.
}
}
+/// 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`.
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> {
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(())
+}
--- /dev/null
+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
+
# 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
--- /dev/null
+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
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