mirror of
https://git.proxmox.com/git/proxmox
synced 2025-08-16 07:38:43 +00:00
add more functions to check repositories
Currently includes check for suites and check for official URIs Signed-off-by: Fabian Ebner <f.ebner@proxmox.com>
This commit is contained in:
parent
5bf8ddece7
commit
76d3a5ba1f
@ -2,10 +2,13 @@ use std::convert::TryFrom;
|
|||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use anyhow::{format_err, Error};
|
use anyhow::{bail, format_err, Error};
|
||||||
use serde::{Deserialize, Serialize};
|
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;
|
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 {
|
impl APTRepositoryFile {
|
||||||
/// Creates a new `APTRepositoryFile` without parsing.
|
/// Creates a new `APTRepositoryFile` without parsing.
|
||||||
///
|
///
|
||||||
@ -271,4 +297,93 @@ impl APTRepositoryFile {
|
|||||||
|
|
||||||
Ok(())
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,9 @@ pub use repository::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
mod file;
|
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_FILENAME: &str = "/etc/apt/sources.list";
|
||||||
const APT_SOURCES_LIST_DIRECTORY: &str = "/etc/apt/sources.list.d/";
|
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[..])
|
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
|
/// Returns all APT repositories configured in `/etc/apt/sources.list` and
|
||||||
/// in `/etc/apt/sources.list.d` including disabled repositories.
|
/// in `/etc/apt/sources.list.d` including disabled repositories.
|
||||||
///
|
///
|
||||||
|
42
src/repositories/release.rs
Normal file
42
src/repositories/release.rs
Normal file
@ -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'");
|
||||||
|
}
|
@ -266,6 +266,30 @@ impl APTRepository {
|
|||||||
Ok(())
|
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.
|
/// Writes a repository in the corresponding format followed by a blank.
|
||||||
///
|
///
|
||||||
/// Expects that `basic_check()` for the repository was successful.
|
/// 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.
|
/// Writes a repository in one-line format followed by a blank line.
|
||||||
///
|
///
|
||||||
/// Expects that `repo.file_type == APTRepositoryFileType::List`.
|
/// Expects that `repo.file_type == APTRepositoryFileType::List`.
|
||||||
|
@ -2,7 +2,7 @@ use std::path::PathBuf;
|
|||||||
|
|
||||||
use anyhow::{bail, format_err, Error};
|
use anyhow::{bail, format_err, Error};
|
||||||
|
|
||||||
use proxmox_apt::repositories::APTRepositoryFile;
|
use proxmox_apt::repositories::{check_repositories, APTRepositoryFile, APTRepositoryInfo};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_write() -> Result<(), Error> {
|
fn test_parse_write() -> Result<(), Error> {
|
||||||
@ -160,3 +160,107 @@ fn test_empty_write() -> Result<(), Error> {
|
|||||||
|
|
||||||
Ok(())
|
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(())
|
||||||
|
}
|
||||||
|
30
tests/sources.list.d.expected/bad.sources
Normal file
30
tests/sources.list.d.expected/bad.sources
Normal file
@ -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
|
||||||
|
|
@ -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 https://enterprise.proxmox.com/debian/pve bullseye pve-enterprise
|
||||||
|
|
||||||
|
deb-src https://enterprise.proxmox.com/debian/pve buster pve-enterprise
|
||||||
|
|
||||||
# security updates
|
# security updates
|
||||||
deb http://security.debian.org/debian-security bullseye-security main contrib
|
deb http://security.debian.org/debian-security bullseye-security main contrib
|
||||||
|
|
||||||
|
29
tests/sources.list.d/bad.sources
Normal file
29
tests/sources.list.d/bad.sources
Normal file
@ -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
|
@ -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 http://download.proxmox.com/debian/pve bullseye pve-no-subscription
|
||||||
# deb https://enterprise.proxmox.com/debian/pve bullseye pve-enterprise
|
# deb https://enterprise.proxmox.com/debian/pve bullseye pve-enterprise
|
||||||
|
|
||||||
|
deb-src https://enterprise.proxmox.com/debian/pve buster pve-enterprise
|
||||||
|
|
||||||
# security updates
|
# security updates
|
||||||
deb http://security.debian.org/debian-security bullseye-security main contrib
|
deb http://security.debian.org/debian-security bullseye-security main contrib
|
||||||
|
Loading…
Reference in New Issue
Block a user