apt: use api types from apt-api-types crate

Signed-off-by: Dietmar Maurer <dietmar@proxmox.com>
This commit is contained in:
Dietmar Maurer 2024-07-01 11:22:05 +02:00 committed by Wolfgang Bumiller
parent e56e39185a
commit f536a91b2f
9 changed files with 65 additions and 428 deletions

View File

@ -20,4 +20,5 @@ serde_json.workspace = true
rfc822-like = "0.2.1"
proxmox-schema = { workspace = true, features = [ "api-macro" ] }
proxmox-apt-api-types.workspace = true
proxmox-config-digest = { workspace = true, features = ["openssl"] }

View File

@ -10,8 +10,9 @@ Build-Depends: debhelper (>= 12),
librust-hex-0.4+default-dev <!nocheck>,
librust-once-cell-1+default-dev (>= 1.3.1-~~) <!nocheck>,
librust-openssl-0.10+default-dev <!nocheck>,
librust-proxmox-schema-3+api-macro-dev (>= 3.1.1-~~) <!nocheck>,
librust-proxmox-schema-3+default-dev (>= 3.1.1-~~) <!nocheck>,
librust-proxmox-apt-api-types-1+default-dev <!nocheck>,
librust-proxmox-config-digest-0.1+default-dev <!nocheck>,
librust-proxmox-config-digest-0.1+openssl-dev <!nocheck>,
librust-rfc822-like-0.2+default-dev (>= 0.2.1-~~) <!nocheck>,
librust-serde-1+default-dev <!nocheck>,
librust-serde-1+derive-dev <!nocheck>,
@ -33,8 +34,9 @@ Depends:
librust-hex-0.4+default-dev,
librust-once-cell-1+default-dev (>= 1.3.1-~~),
librust-openssl-0.10+default-dev,
librust-proxmox-schema-3+api-macro-dev (>= 3.1.1-~~),
librust-proxmox-schema-3+default-dev (>= 3.1.1-~~),
librust-proxmox-apt-api-types-1+default-dev,
librust-proxmox-config-digest-0.1+default-dev,
librust-proxmox-config-digest-0.1+openssl-dev,
librust-rfc822-like-0.2+default-dev (>= 0.2.1-~~),
librust-serde-1+default-dev,
librust-serde-1+derive-dev,

View File

@ -1,123 +1,29 @@
use std::fmt::Display;
use std::path::{Path, PathBuf};
use anyhow::{format_err, Error};
use serde::{Deserialize, Serialize};
use crate::repositories::release::DebianCodename;
use crate::repositories::repository::{
APTRepository, APTRepositoryFileType, APTRepositoryPackageType,
use proxmox_apt_api_types::{
APTRepository, APTRepositoryFile, APTRepositoryFileError, APTRepositoryFileType,
APTRepositoryInfo, APTRepositoryPackageType,
};
use crate::repositories::repository::APTRepositoryImpl;
use proxmox_schema::api;
mod list_parser;
use list_parser::APTListFileParser;
mod sources_parser;
use sources_parser::APTSourcesFileParser;
use proxmox_config_digest::ConfigDigest;
trait APTRepositoryParser {
/// Parse all repositories including the disabled ones and push them onto
/// the provided vector.
fn parse_repositories(&mut self) -> Result<Vec<APTRepository>, Error>;
}
#[api(
properties: {
"file-type": {
type: APTRepositoryFileType,
},
repositories: {
description: "List of APT repositories.",
type: Array,
items: {
type: APTRepository,
},
},
digest: {
description: "Digest for the content of the file.",
optional: true,
type: Array,
items: {
description: "Digest byte.",
type: u8,
},
},
},
)]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
/// Represents an abstract APT repository file.
pub struct APTRepositoryFile {
/// The path to the file. If None, `contents` must be set directly.
#[serde(skip_serializing_if = "Option::is_none")]
pub path: Option<String>,
/// The type of the file.
pub file_type: APTRepositoryFileType,
/// List of repositories in the file.
pub repositories: Vec<APTRepository>,
/// The file content, if already parsed.
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
/// Digest of the original contents.
#[serde(skip_serializing_if = "Option::is_none")]
pub digest: Option<[u8; 32]>,
}
#[api]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
/// Error type for problems with APT repository files.
pub struct APTRepositoryFileError {
/// The path to the problematic file.
pub path: String,
/// The error message.
pub error: String,
}
impl Display for APTRepositoryFileError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "proxmox-apt error for '{}' - {}", self.path, self.error)
}
}
impl std::error::Error for APTRepositoryFileError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
None
}
}
#[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(default, 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,
}
pub trait APTRepositoryFileImpl {
/// Creates a new `APTRepositoryFile` without parsing.
///
@ -131,7 +37,7 @@ pub trait APTRepositoryFileImpl {
/// Check if the file exists.
fn exists(&self) -> bool;
fn read_with_digest(&self) -> Result<(Vec<u8>, [u8; 32]), APTRepositoryFileError>;
fn read_with_digest(&self) -> Result<(Vec<u8>, ConfigDigest), APTRepositoryFileError>;
/// Create an `APTRepositoryFileError`.
fn err(&self, error: Error) -> APTRepositoryFileError;
@ -213,7 +119,8 @@ impl APTRepositoryFileImpl for APTRepositoryFile {
return Ok(None);
}
let file_type = APTRepositoryFileType::try_from(&extension[..])
let file_type = extension[..]
.parse()
.map_err(|_| new_err("invalid extension"))?;
if !file_name
@ -250,15 +157,15 @@ impl APTRepositoryFileImpl for APTRepositoryFile {
}
}
fn read_with_digest(&self) -> Result<(Vec<u8>, [u8; 32]), APTRepositoryFileError> {
fn read_with_digest(&self) -> Result<(Vec<u8>, ConfigDigest), APTRepositoryFileError> {
if let Some(path) = &self.path {
let content = std::fs::read(path).map_err(|err| self.err(format_err!("{}", err)))?;
let digest = openssl::sha::sha256(&content);
let digest = ConfigDigest::from_slice(&content);
Ok((content, digest))
} else if let Some(ref content) = self.content {
let content = content.as_bytes();
let digest = openssl::sha::sha256(content);
let digest = ConfigDigest::from_slice(content);
Ok((content.to_vec(), digest))
} else {
Err(self.err(format_err!(
@ -308,13 +215,13 @@ impl APTRepositoryFileImpl for APTRepositoryFile {
}
};
if let Some(digest) = self.digest {
if let Some(digest) = &self.digest {
if !self.exists() {
return Err(self.err(format_err!("digest specified, but file does not exist")));
}
let (_, current_digest) = self.read_with_digest()?;
if digest != current_digest {
if digest != &current_digest {
return Err(self.err(format_err!("digest mismatch")));
}
}

View File

@ -184,7 +184,7 @@ impl<R: BufRead> APTListFileParser<R> {
// e.g. quoted "deb" is not accepted by APT, so no need for quote word parsing here
line = match line.split_once(|c| char::is_ascii_whitespace(&c)) {
Some((package_type, rest)) => {
repo.types.push(package_type.try_into()?);
repo.types.push(package_type.parse()?);
rest
}
None => return Ok(None), // empty line

View File

@ -108,7 +108,7 @@ impl<R: BufRead> APTSourcesFileParser<R> {
}
let mut types = Vec::<APTRepositoryPackageType>::new();
for package_type in values {
types.push((&package_type[..]).try_into()?);
types.push((&package_type[..]).parse()?);
}
repo.types = types;
}

View File

@ -4,21 +4,22 @@ use std::path::PathBuf;
use anyhow::{bail, Error};
mod repository;
pub use repository::APTRepositoryImpl;
pub use repository::{
APTRepository, APTRepositoryFileType, APTRepositoryOption, APTRepositoryPackageType,
use proxmox_apt_api_types::{
APTRepository, APTRepositoryFile, APTRepositoryFileError, APTRepositoryFileType,
APTRepositoryHandle, APTRepositoryInfo, APTRepositoryOption, APTRepositoryPackageType,
APTStandardRepository,
};
use proxmox_config_digest::ConfigDigest;
pub use repository::APTRepositoryImpl;
mod file;
pub use file::APTRepositoryFileImpl;
pub use file::{APTRepositoryFile, APTRepositoryFileError, APTRepositoryInfo};
mod release;
pub use release::{get_current_release_codename, DebianCodename};
mod standard;
pub use standard::APTRepositoryHandleImpl;
pub use standard::{APTRepositoryHandle, APTStandardRepository};
pub use standard::{APTRepositoryHandleImpl, APTStandardRepositoryImpl};
const APT_SOURCES_LIST_FILENAME: &str = "/etc/apt/sources.list";
const APT_SOURCES_LIST_DIRECTORY: &str = "/etc/apt/sources.list.d/";
@ -28,7 +29,7 @@ const APT_SOURCES_LIST_DIRECTORY: &str = "/etc/apt/sources.list.d/";
/// The digest is invariant with respect to file order.
///
/// Files without a digest are ignored.
fn common_digest(files: &[APTRepositoryFile]) -> [u8; 32] {
fn common_digest(files: &[APTRepositoryFile]) -> ConfigDigest {
let mut digests = BTreeMap::new();
for file in files.iter() {
@ -43,7 +44,7 @@ fn common_digest(files: &[APTRepositoryFile]) -> [u8; 32] {
}
}
openssl::sha::sha256(&common_raw[..])
ConfigDigest::from_slice(&common_raw[..])
}
/// Provides additional information about the repositories.
@ -86,22 +87,22 @@ pub fn standard_repositories(
suite: DebianCodename,
) -> Vec<APTStandardRepository> {
let mut result = vec![
APTStandardRepository::from(APTRepositoryHandle::Enterprise),
APTStandardRepository::from(APTRepositoryHandle::NoSubscription),
APTStandardRepository::from(APTRepositoryHandle::Test),
APTStandardRepository::from_handle(APTRepositoryHandle::Enterprise),
APTStandardRepository::from_handle(APTRepositoryHandle::NoSubscription),
APTStandardRepository::from_handle(APTRepositoryHandle::Test),
];
if product == "pve" {
result.append(&mut vec![
APTStandardRepository::from(APTRepositoryHandle::CephQuincyEnterprise),
APTStandardRepository::from(APTRepositoryHandle::CephQuincyNoSubscription),
APTStandardRepository::from(APTRepositoryHandle::CephQuincyTest),
APTStandardRepository::from_handle(APTRepositoryHandle::CephQuincyEnterprise),
APTStandardRepository::from_handle(APTRepositoryHandle::CephQuincyNoSubscription),
APTStandardRepository::from_handle(APTRepositoryHandle::CephQuincyTest),
]);
if suite == DebianCodename::Bookworm {
result.append(&mut vec![
APTStandardRepository::from(APTRepositoryHandle::CephReefEnterprise),
APTStandardRepository::from(APTRepositoryHandle::CephReefNoSubscription),
APTStandardRepository::from(APTRepositoryHandle::CephReefTest),
APTStandardRepository::from_handle(APTRepositoryHandle::CephReefEnterprise),
APTStandardRepository::from_handle(APTRepositoryHandle::CephReefNoSubscription),
APTStandardRepository::from_handle(APTRepositoryHandle::CephReefTest),
]);
}
}
@ -128,7 +129,7 @@ pub fn standard_repositories(
pub type Repositories = (
Vec<APTRepositoryFile>,
Vec<APTRepositoryFileError>,
[u8; 32],
ConfigDigest,
);
/// Returns all APT repositories configured in `/etc/apt/sources.list` and

View File

@ -1,193 +1,12 @@
use std::fmt::Display;
use std::io::{BufRead, BufReader, Write};
use std::path::PathBuf;
use anyhow::{bail, format_err, Error};
use serde::{Deserialize, Serialize};
use proxmox_schema::api;
use crate::repositories::standard::APTRepositoryHandle;
use crate::repositories::standard::APTRepositoryHandleImpl;
#[api]
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum APTRepositoryFileType {
/// One-line-style format
List,
/// DEB822-style format
Sources,
}
impl TryFrom<&str> for APTRepositoryFileType {
type Error = Error;
fn try_from(file_type: &str) -> Result<Self, Error> {
match file_type {
"list" => Ok(APTRepositoryFileType::List),
"sources" => Ok(APTRepositoryFileType::Sources),
_ => bail!("invalid file type '{file_type}'"),
}
}
}
impl Display for APTRepositoryFileType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
APTRepositoryFileType::List => write!(f, "list"),
APTRepositoryFileType::Sources => write!(f, "sources"),
}
}
}
#[api]
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum APTRepositoryPackageType {
/// Debian package
Deb,
/// Debian source package
DebSrc,
}
impl TryFrom<&str> for APTRepositoryPackageType {
type Error = Error;
fn try_from(package_type: &str) -> Result<Self, Error> {
match package_type {
"deb" => Ok(APTRepositoryPackageType::Deb),
"deb-src" => Ok(APTRepositoryPackageType::DebSrc),
_ => bail!("invalid package type '{package_type}'"),
}
}
}
impl Display for APTRepositoryPackageType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
APTRepositoryPackageType::Deb => write!(f, "deb"),
APTRepositoryPackageType::DebSrc => write!(f, "deb-src"),
}
}
}
#[api(
properties: {
Key: {
description: "Option key.",
type: String,
},
Values: {
description: "Option values.",
type: Array,
items: {
description: "Value.",
type: String,
},
},
},
)]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")] // for consistency
/// Additional options for an APT repository.
/// Used for both single- and mutli-value options.
pub struct APTRepositoryOption {
/// Option key.
pub key: String,
/// Option value(s).
pub values: Vec<String>,
}
#[api(
properties: {
Types: {
description: "List of package types.",
type: Array,
items: {
type: APTRepositoryPackageType,
},
},
URIs: {
description: "List of repository URIs.",
type: Array,
items: {
description: "Repository URI.",
type: String,
},
},
Suites: {
description: "List of distributions.",
type: Array,
items: {
description: "Package distribution.",
type: String,
},
},
Components: {
description: "List of repository components.",
type: Array,
items: {
description: "Repository component.",
type: String,
},
},
Options: {
type: Array,
optional: true,
items: {
type: APTRepositoryOption,
},
},
Comment: {
description: "Associated comment.",
type: String,
optional: true,
},
FileType: {
type: APTRepositoryFileType,
},
Enabled: {
description: "Whether the repository is enabled or not.",
type: Boolean,
},
},
)]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
/// Describes an APT repository.
pub struct APTRepository {
/// List of package types.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub types: Vec<APTRepositoryPackageType>,
/// List of repository URIs.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[serde(rename = "URIs")]
pub uris: Vec<String>,
/// List of package distributions.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub suites: Vec<String>,
/// List of repository components.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub components: Vec<String>,
/// Additional options.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub options: Vec<APTRepositoryOption>,
/// Associated comment.
#[serde(default, skip_serializing_if = "String::is_empty")]
pub comment: String,
/// Format of the defining file.
pub file_type: APTRepositoryFileType,
/// Whether the repository is enabled or not.
pub enabled: bool,
}
use proxmox_apt_api_types::{
APTRepository, APTRepositoryFileType, APTRepositoryHandle, APTRepositoryOption,
};
pub trait APTRepositoryImpl {
/// Crates an empty repository.

View File

@ -1,70 +1,14 @@
use std::fmt::Display;
use anyhow::{bail, Error};
use serde::{Deserialize, Serialize};
use crate::repositories::repository::{
APTRepository, APTRepositoryFileType, APTRepositoryPackageType,
use proxmox_apt_api_types::{
APTRepository, APTRepositoryFileType, APTRepositoryHandle, APTRepositoryPackageType,
APTStandardRepository,
};
use proxmox_schema::api;
#[api(
properties: {
handle: {
description: "Handle referencing a standard repository.",
type: String,
},
},
)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
/// Reference to a standard repository and configuration status.
pub struct APTStandardRepository {
/// Handle referencing a standard repository.
pub handle: APTRepositoryHandle,
/// Configuration status of the associated repository, where `None` means
/// not configured, and `Some(bool)` indicates enabled or disabled.
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<bool>,
/// Display name of the repository.
pub name: String,
/// Description of the repository.
pub description: String,
pub trait APTStandardRepositoryImpl {
fn from_handle(handle: APTRepositoryHandle) -> APTStandardRepository;
}
#[api]
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
/// Handles for Proxmox repositories.
pub enum APTRepositoryHandle {
/// The enterprise repository for production use.
Enterprise,
/// The repository that can be used without subscription.
NoSubscription,
/// The test repository.
Test,
/// Ceph Quincy enterprise repository.
CephQuincyEnterprise,
/// Ceph Quincy no-subscription repository.
CephQuincyNoSubscription,
/// Ceph Quincy test repository.
CephQuincyTest,
// TODO: Add separate enum for ceph releases and use something like
// `CephTest(CephReleaseCodename),` once the API macro supports it.
/// Ceph Reef enterprise repository.
CephReefEnterprise,
/// Ceph Reef no-subscription repository.
CephReefNoSubscription,
/// Ceph Reef test repository.
CephReefTest,
}
impl From<APTRepositoryHandle> for APTStandardRepository {
fn from(handle: APTRepositoryHandle) -> Self {
impl APTStandardRepositoryImpl for APTStandardRepository {
fn from_handle(handle: APTRepositoryHandle) -> APTStandardRepository {
APTStandardRepository {
handle,
status: None,
@ -74,43 +18,6 @@ impl From<APTRepositoryHandle> for APTStandardRepository {
}
}
impl TryFrom<&str> for APTRepositoryHandle {
type Error = Error;
fn try_from(string: &str) -> Result<Self, Error> {
match string {
"enterprise" => Ok(APTRepositoryHandle::Enterprise),
"no-subscription" => Ok(APTRepositoryHandle::NoSubscription),
"test" => Ok(APTRepositoryHandle::Test),
"ceph-quincy-enterprise" => Ok(APTRepositoryHandle::CephQuincyEnterprise),
"ceph-quincy-no-subscription" => Ok(APTRepositoryHandle::CephQuincyNoSubscription),
"ceph-quincy-test" => Ok(APTRepositoryHandle::CephQuincyTest),
"ceph-reef-enterprise" => Ok(APTRepositoryHandle::CephReefEnterprise),
"ceph-reef-no-subscription" => Ok(APTRepositoryHandle::CephReefNoSubscription),
"ceph-reef-test" => Ok(APTRepositoryHandle::CephReefTest),
_ => bail!("unknown repository handle '{}'", string),
}
}
}
impl Display for APTRepositoryHandle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
APTRepositoryHandle::Enterprise => write!(f, "enterprise"),
APTRepositoryHandle::NoSubscription => write!(f, "no-subscription"),
APTRepositoryHandle::Test => write!(f, "test"),
APTRepositoryHandle::CephQuincyEnterprise => write!(f, "ceph-quincy-enterprise"),
APTRepositoryHandle::CephQuincyNoSubscription => {
write!(f, "ceph-quincy-no-subscription")
}
APTRepositoryHandle::CephQuincyTest => write!(f, "ceph-quincy-test"),
APTRepositoryHandle::CephReefEnterprise => write!(f, "ceph-reef-enterprise"),
APTRepositoryHandle::CephReefNoSubscription => write!(f, "ceph-reef-no-subscription"),
APTRepositoryHandle::CephReefTest => write!(f, "ceph-reef-test"),
}
}
}
pub trait APTRepositoryHandleImpl {
/// Get the description for the repository.
fn description(self) -> String;

View File

@ -9,7 +9,7 @@ use proxmox_apt::repositories::{
APTRepositoryHandle, APTRepositoryInfo, APTStandardRepository, DebianCodename,
};
use proxmox_apt::repositories::{
APTRepositoryFileImpl, APTRepositoryHandleImpl, APTRepositoryImpl,
APTRepositoryFileImpl, APTRepositoryHandleImpl, APTRepositoryImpl, APTStandardRepositoryImpl,
};
fn create_clean_directory(path: &PathBuf) -> Result<(), Error> {
@ -114,7 +114,7 @@ fn test_digest() -> Result<(), Error> {
let new_path = write_dir.join(path.file_name().unwrap());
file.path = Some(new_path.clone().into_os_string().into_string().unwrap());
let old_digest = file.digest.unwrap();
let old_digest = file.digest.clone().unwrap();
// file does not exist yet...
assert!(file.read_with_digest().is_err());
@ -132,7 +132,7 @@ fn test_digest() -> Result<(), Error> {
repo.enabled = !repo.enabled;
// ...then it should work
file.digest = Some(old_digest);
file.digest = Some(old_digest.clone());
file.write()?;
// expect a different digest, because the repo was modified
@ -361,15 +361,15 @@ fn test_standard_repositories() -> Result<(), Error> {
let read_dir = test_dir.join("sources.list.d");
let mut expected = vec![
APTStandardRepository::from(APTRepositoryHandle::Enterprise),
APTStandardRepository::from(APTRepositoryHandle::NoSubscription),
APTStandardRepository::from(APTRepositoryHandle::Test),
APTStandardRepository::from(APTRepositoryHandle::CephQuincyEnterprise),
APTStandardRepository::from(APTRepositoryHandle::CephQuincyNoSubscription),
APTStandardRepository::from(APTRepositoryHandle::CephQuincyTest),
APTStandardRepository::from(APTRepositoryHandle::CephReefEnterprise),
APTStandardRepository::from(APTRepositoryHandle::CephReefNoSubscription),
APTStandardRepository::from(APTRepositoryHandle::CephReefTest),
APTStandardRepository::from_handle(APTRepositoryHandle::Enterprise),
APTStandardRepository::from_handle(APTRepositoryHandle::NoSubscription),
APTStandardRepository::from_handle(APTRepositoryHandle::Test),
APTStandardRepository::from_handle(APTRepositoryHandle::CephQuincyEnterprise),
APTStandardRepository::from_handle(APTRepositoryHandle::CephQuincyNoSubscription),
APTStandardRepository::from_handle(APTRepositoryHandle::CephQuincyTest),
APTStandardRepository::from_handle(APTRepositoryHandle::CephReefEnterprise),
APTStandardRepository::from_handle(APTRepositoryHandle::CephReefNoSubscription),
APTStandardRepository::from_handle(APTRepositoryHandle::CephReefTest),
];
let absolute_suite_list = read_dir.join("absolute_suite.list");