* [pve-devel] [PATCH proxmox v3 01/13] io: introduce RangeReader for bounded reads
2025-07-09 12:34 [pve-devel] [PATCH container/docs/lxcfs/manager/proxmox{, -perl-rs}/storage v3 00/13] support OCI images as container templates Filip Schauer
@ 2025-07-09 12:34 ` Filip Schauer
2025-07-10 6:04 ` Thomas Lamprecht
2025-07-09 12:34 ` [pve-devel] [PATCH proxmox v3 02/13] add proxmox-oci crate Filip Schauer
` (11 subsequent siblings)
12 siblings, 1 reply; 21+ messages in thread
From: Filip Schauer @ 2025-07-09 12:34 UTC (permalink / raw)
To: pve-devel
Signed-off-by: Filip Schauer <f.schauer@proxmox.com>
---
Introduced in v3
proxmox-io/src/lib.rs | 3 ++
proxmox-io/src/range_reader.rs | 94 ++++++++++++++++++++++++++++++++++
2 files changed, 97 insertions(+)
create mode 100644 proxmox-io/src/range_reader.rs
diff --git a/proxmox-io/src/lib.rs b/proxmox-io/src/lib.rs
index 1be005ff..a05b9232 100644
--- a/proxmox-io/src/lib.rs
+++ b/proxmox-io/src/lib.rs
@@ -6,6 +6,9 @@
#![deny(unsafe_op_in_unsafe_fn)]
#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
+mod range_reader;
+pub use range_reader::RangeReader;
+
mod read;
pub use read::ReadExt;
diff --git a/proxmox-io/src/range_reader.rs b/proxmox-io/src/range_reader.rs
new file mode 100644
index 00000000..307bfe27
--- /dev/null
+++ b/proxmox-io/src/range_reader.rs
@@ -0,0 +1,94 @@
+use std::io::{Read, Seek, SeekFrom};
+use std::ops::Range;
+
+pub struct RangeReader<R: Read + Seek> {
+ reader: R,
+
+ /// Range inside `R`.
+ range: Range<u64>,
+
+ /// Relative position inside `range`
+ position: u64,
+
+ /// True once the initial seek has been performed
+ ready: bool,
+}
+
+impl<R: Read + Seek> RangeReader<R> {
+ pub fn new(reader: R, range: Range<u64>) -> Self {
+ Self {
+ reader,
+ range,
+ position: 0,
+ ready: false,
+ }
+ }
+
+ pub fn into_inner(self) -> R {
+ self.reader
+ }
+
+ pub fn size(&self) -> usize {
+ (self.range.end - self.range.start) as usize
+ }
+
+ pub fn remaining(&self) -> usize {
+ self.size() - self.position as usize
+ }
+}
+
+impl<R: Read + Seek> Read for RangeReader<R> {
+ fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
+ let max_read = buf.len().min(self.remaining());
+ let limited_buf = &mut buf[..max_read];
+
+ if !self.ready {
+ self.reader
+ .seek(SeekFrom::Start(self.range.start + self.position))?;
+ self.ready = true;
+ }
+
+ let bytes_read = self.reader.read(limited_buf)?;
+ self.position += bytes_read.min(max_read) as u64;
+
+ Ok(bytes_read)
+ }
+}
+
+impl<R: Read + Seek> Seek for RangeReader<R> {
+ fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
+ self.position = match pos {
+ SeekFrom::Start(position) => position.min(self.size() as u64),
+ SeekFrom::End(offset) => {
+ if offset > self.size() as i64 {
+ return Err(std::io::Error::new(
+ std::io::ErrorKind::InvalidInput,
+ "Tried to seek before the beginning of the file",
+ ));
+ }
+
+ (if offset <= 0 {
+ self.size()
+ } else {
+ self.size() - offset as usize
+ }) as u64
+ }
+ SeekFrom::Current(offset) => {
+ if let Some(position) = self.position.checked_add_signed(offset) {
+ position.min(self.size() as u64)
+ } else {
+ return Err(std::io::Error::new(
+ std::io::ErrorKind::InvalidInput,
+ "Tried to seek before the beginning of the file",
+ ));
+ }
+ }
+ };
+
+ self.reader
+ .seek(SeekFrom::Start(self.range.start + self.position))?;
+ self.ready = true;
+
+ Ok(self.position)
+ }
+}
--
2.47.2
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 21+ messages in thread
* Re: [pve-devel] [PATCH proxmox v3 01/13] io: introduce RangeReader for bounded reads
2025-07-09 12:34 ` [pve-devel] [PATCH proxmox v3 01/13] io: introduce RangeReader for bounded reads Filip Schauer
@ 2025-07-10 6:04 ` Thomas Lamprecht
0 siblings, 0 replies; 21+ messages in thread
From: Thomas Lamprecht @ 2025-07-10 6:04 UTC (permalink / raw)
To: Proxmox VE development discussion, Filip Schauer
There's zero actual commit message and basically no rustdoc comment for
the public interface, that needs to improve, especially library crates
should be held to higher standards in this regard.
non-exhaustive list of things that I'd find relevant for such crates:
- describing the background in the commit message (where this will be used,
why is it worth having, can we, e.g., replace some existing code with this)
- short overview as rustdoc comment, here either module wide or on the type.
- adding a (short) rustdoc example for how this would be used.
- some basic unit tests, as the Read trait is implemented for &[u8], this
would be doable here without IO side effects for the underlying build
system and make review of edge cases much easier.
I know you're a bit in a rush to advance this series, and we do not have
to get this fine-polished for initial inclusion; but investing roughly a
half an hour consisting of 10 minutes for a _bit_ more rustdoc and 20
minutes for some very basic tests and might get us a long way here
Am 09.07.25 um 14:34 schrieb Filip Schauer:
> diff --git a/proxmox-io/src/range_reader.rs b/proxmox-io/src/range_reader.rs
> new file mode 100644
> index 00000000..307bfe27
> --- /dev/null
> +++ b/proxmox-io/src/range_reader.rs
> @@ -0,0 +1,94 @@
> +use std::io::{Read, Seek, SeekFrom};
> +use std::ops::Range;
> +
> +pub struct RangeReader<R: Read + Seek> {
> + reader: R,
> +
> + /// Range inside `R`.
> + range: Range<u64>,
> +
> + /// Relative position inside `range`
nit: Inconsistent use of trailing sentence point, one above got one but this
here and below doesn't.
> + position: u64,
> +
> + /// True once the initial seek has been performed
> + ready: bool,
nit: It's internal, so not _that_ important, but might be slightly nicer to be
very explicit with the name, e.g. seeked_once or first_seek_done, as ready
is generic and opaque, people can interpret this widely different, and while
we at least got some rustdoc here (thx to Wolfgang including it in his
suggestion to add this in his review for the previous version I guess ;), it's
still nice to have that communicated in the code that uses this.
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 21+ messages in thread
* [pve-devel] [PATCH proxmox v3 02/13] add proxmox-oci crate
2025-07-09 12:34 [pve-devel] [PATCH container/docs/lxcfs/manager/proxmox{, -perl-rs}/storage v3 00/13] support OCI images as container templates Filip Schauer
2025-07-09 12:34 ` [pve-devel] [PATCH proxmox v3 01/13] io: introduce RangeReader for bounded reads Filip Schauer
@ 2025-07-09 12:34 ` Filip Schauer
2025-07-10 8:46 ` Wolfgang Bumiller
2025-07-09 12:34 ` [pve-devel] [PATCH proxmox-perl-rs v3 03/13] add Perl mapping for OCI container image parser/extractor Filip Schauer
` (10 subsequent siblings)
12 siblings, 1 reply; 21+ messages in thread
From: Filip Schauer @ 2025-07-09 12:34 UTC (permalink / raw)
To: pve-devel
This crate can parse an OCI image tarball and extract its rootfs. Layers
are applied in sequence, but an overlay filesystem is currently not
used.
Signed-off-by: Filip Schauer <f.schauer@proxmox.com>
---
Changed since v2:
* remove reachable unwraps & refactor code
* increase hasher buffer size from 4096 to 32768 (matching internal
sha2::Digest buffering)
* preserve permissions and xattrs during rootfs extraction
* handle whiteouts & opaque whiteouts
Cargo.toml | 1 +
proxmox-oci/Cargo.toml | 22 +++
proxmox-oci/debian/changelog | 5 +
proxmox-oci/debian/control | 45 +++++
proxmox-oci/debian/debcargo.toml | 7 +
proxmox-oci/src/lib.rs | 283 +++++++++++++++++++++++++++++++
proxmox-oci/src/oci_tar_image.rs | 145 ++++++++++++++++
7 files changed, 508 insertions(+)
create mode 100644 proxmox-oci/Cargo.toml
create mode 100644 proxmox-oci/debian/changelog
create mode 100644 proxmox-oci/debian/control
create mode 100644 proxmox-oci/debian/debcargo.toml
create mode 100644 proxmox-oci/src/lib.rs
create mode 100644 proxmox-oci/src/oci_tar_image.rs
diff --git a/Cargo.toml b/Cargo.toml
index 020e7497..4606fc19 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -26,6 +26,7 @@ members = [
"proxmox-metrics",
"proxmox-network-api",
"proxmox-notify",
+ "proxmox-oci",
"proxmox-openid",
"proxmox-product-config",
"proxmox-resource-scheduling",
diff --git a/proxmox-oci/Cargo.toml b/proxmox-oci/Cargo.toml
new file mode 100644
index 00000000..4daff6ab
--- /dev/null
+++ b/proxmox-oci/Cargo.toml
@@ -0,0 +1,22 @@
+[package]
+name = "proxmox-oci"
+description = "OCI image parsing and extraction"
+version = "0.1.0"
+
+authors.workspace = true
+edition.workspace = true
+exclude.workspace = true
+homepage.workspace = true
+license.workspace = true
+repository.workspace = true
+rust-version.workspace = true
+
+[dependencies]
+flate2.workspace = true
+oci-spec = "0.8.1"
+sha2 = "0.10"
+tar.workspace = true
+thiserror = "1"
+zstd.workspace = true
+
+proxmox-io.workspace = true
diff --git a/proxmox-oci/debian/changelog b/proxmox-oci/debian/changelog
new file mode 100644
index 00000000..754d06c1
--- /dev/null
+++ b/proxmox-oci/debian/changelog
@@ -0,0 +1,5 @@
+rust-proxmox-oci (0.1.0-1) bookworm; urgency=medium
+
+ * Initial release.
+
+ -- Proxmox Support Team <support@proxmox.com> Mon, 28 Apr 2025 12:34:56 +0200
diff --git a/proxmox-oci/debian/control b/proxmox-oci/debian/control
new file mode 100644
index 00000000..f33331c5
--- /dev/null
+++ b/proxmox-oci/debian/control
@@ -0,0 +1,45 @@
+Source: rust-proxmox-oci
+Section: rust
+Priority: optional
+Build-Depends: debhelper-compat (= 13),
+ dh-sequence-cargo
+Build-Depends-Arch: cargo:native <!nocheck>,
+ rustc:native (>= 1.82) <!nocheck>,
+ libstd-rust-dev <!nocheck>,
+ librust-flate2-1+default-dev <!nocheck>,
+ librust-oci-spec-0.8+default-dev (>= 0.8.1-~~) <!nocheck>,
+ librust-proxmox-io-1+default-dev (>= 1.2.0-~~) <!nocheck>,
+ librust-sha2-0.10+default-dev <!nocheck>,
+ librust-tar-0.4+default-dev <!nocheck>,
+ librust-thiserror-1+default-dev <!nocheck>,
+ librust-zstd-0.13+default-dev <!nocheck>
+Maintainer: Proxmox Support Team <support@proxmox.com>
+Standards-Version: 4.7.0
+Vcs-Git: git://git.proxmox.com/git/proxmox.git
+Vcs-Browser: https://git.proxmox.com/?p=proxmox.git
+Homepage: https://proxmox.com
+X-Cargo-Crate: proxmox-oci
+Rules-Requires-Root: no
+
+Package: librust-proxmox-oci-dev
+Architecture: any
+Multi-Arch: same
+Depends:
+ ${misc:Depends},
+ librust-flate2-1+default-dev,
+ librust-oci-spec-0.8+default-dev (>= 0.8.1-~~),
+ librust-proxmox-io-1+default-dev (>= 1.2.0-~~),
+ librust-sha2-0.10+default-dev,
+ librust-tar-0.4+default-dev,
+ librust-thiserror-1+default-dev,
+ librust-zstd-0.13+default-dev
+Provides:
+ librust-proxmox-oci+default-dev (= ${binary:Version}),
+ librust-proxmox-oci-0-dev (= ${binary:Version}),
+ librust-proxmox-oci-0+default-dev (= ${binary:Version}),
+ librust-proxmox-oci-0.1-dev (= ${binary:Version}),
+ librust-proxmox-oci-0.1+default-dev (= ${binary:Version}),
+ librust-proxmox-oci-0.1.0-dev (= ${binary:Version}),
+ librust-proxmox-oci-0.1.0+default-dev (= ${binary:Version})
+Description: OCI image parsing and extraction - Rust source code
+ Source code for Debianized Rust crate "proxmox-oci"
diff --git a/proxmox-oci/debian/debcargo.toml b/proxmox-oci/debian/debcargo.toml
new file mode 100644
index 00000000..b7864cdb
--- /dev/null
+++ b/proxmox-oci/debian/debcargo.toml
@@ -0,0 +1,7 @@
+overlay = "."
+crate_src_path = ".."
+maintainer = "Proxmox Support Team <support@proxmox.com>"
+
+[source]
+vcs_git = "git://git.proxmox.com/git/proxmox.git"
+vcs_browser = "https://git.proxmox.com/?p=proxmox.git"
diff --git a/proxmox-oci/src/lib.rs b/proxmox-oci/src/lib.rs
new file mode 100644
index 00000000..b54d06e1
--- /dev/null
+++ b/proxmox-oci/src/lib.rs
@@ -0,0 +1,283 @@
+use std::collections::HashMap;
+use std::fs::{read_dir, remove_dir_all, remove_file, File};
+use std::io::{Read, Seek};
+use std::path::{Path, PathBuf};
+use std::str::FromStr;
+
+use flate2::read::GzDecoder;
+pub use oci_spec::image::Config;
+use oci_spec::image::{Arch, ImageConfiguration, ImageManifest, MediaType};
+use oci_spec::OciSpecError;
+use sha2::digest::generic_array::GenericArray;
+use sha2::{Digest, Sha256};
+use tar::{Archive, EntryType};
+use thiserror::Error;
+
+mod oci_tar_image;
+use oci_tar_image::OciTarImage;
+
+fn compute_digest<R: Read, H: Digest>(
+ mut reader: R,
+ mut hasher: H,
+) -> std::io::Result<GenericArray<u8, H::OutputSize>> {
+ let mut buf = proxmox_io::boxed::zeroed(32768);
+
+ loop {
+ let bytes_read = reader.read(&mut buf)?;
+ if bytes_read == 0 {
+ break Ok(hasher.finalize());
+ }
+
+ hasher.update(&buf[..bytes_read]);
+ }
+}
+
+fn compute_sha256<R: Read>(reader: R) -> std::io::Result<oci_spec::image::Sha256Digest> {
+ let digest = compute_digest(reader, Sha256::new())?;
+ Ok(oci_spec::image::Sha256Digest::from_str(&format!("{digest:x}")).unwrap())
+}
+
+/// Build a mapping from uncompressed layer digests (as found in the image config's `rootfs.diff_ids`)
+/// to their corresponding compressed-layer digests (i.e. the filenames under `blobs/<algorithm>/<digest>`)
+fn build_layer_map<R: Read + Seek>(
+ mut oci_tar_image: OciTarImage<R>,
+ image_manifest: &ImageManifest,
+) -> Result<
+ (
+ OciTarImage<R>,
+ HashMap<oci_spec::image::Digest, oci_spec::image::Descriptor>,
+ ),
+ ExtractError,
+> {
+ let mut layer_mapping = HashMap::new();
+
+ for layer in image_manifest.layers() {
+ let digest = match layer.media_type() {
+ MediaType::ImageLayer | MediaType::ImageLayerNonDistributable => layer.digest().clone(),
+ MediaType::ImageLayerGzip | MediaType::ImageLayerNonDistributableGzip => {
+ let mut compressed_blob = oci_tar_image
+ .open_blob(layer.digest())
+ .ok_or(ExtractError::MissingLayerFile(layer.digest().clone()))?;
+ let decoder = GzDecoder::new(&mut compressed_blob);
+ let hash = compute_sha256(decoder)?.into();
+ oci_tar_image = compressed_blob.into_oci_tar_image();
+ hash
+ }
+ MediaType::ImageLayerZstd | MediaType::ImageLayerNonDistributableZstd => {
+ let mut compressed_blob = oci_tar_image
+ .open_blob(layer.digest())
+ .ok_or(ExtractError::MissingLayerFile(layer.digest().clone()))?;
+ let decoder = zstd::Decoder::new(&mut compressed_blob)?;
+ let hash = compute_sha256(decoder)?.into();
+ oci_tar_image = compressed_blob.into_oci_tar_image();
+ hash
+ }
+ // Skip any other non-ImageLayer related media types.
+ // Match explicitly to avoid missing new image layer types when oci-spec updates.
+ MediaType::Descriptor
+ | MediaType::LayoutHeader
+ | MediaType::ImageManifest
+ | MediaType::ImageIndex
+ | MediaType::ImageConfig
+ | MediaType::ArtifactManifest
+ | MediaType::EmptyJSON
+ | MediaType::Other(_) => continue,
+ };
+
+ layer_mapping.insert(digest, layer.clone());
+ }
+
+ Ok((oci_tar_image, layer_mapping))
+}
+
+#[derive(Debug, Error)]
+pub enum ProxmoxOciError {
+ #[error("Error while parsing OCI image: {0}")]
+ ParseError(#[from] ParseError),
+ #[error("Error while extracting OCI image: {0}")]
+ ExtractError(#[from] ExtractError),
+}
+
+pub fn parse_and_extract_image<P: AsRef<Path>>(
+ oci_tar_path: P,
+ rootfs_path: P,
+) -> Result<Option<Config>, ProxmoxOciError> {
+ let (oci_tar_image, image_manifest, image_config) = parse_image(oci_tar_path)?;
+
+ extract_image_rootfs(oci_tar_image, &image_manifest, &image_config, rootfs_path)?;
+
+ Ok(image_config.config().clone())
+}
+
+#[derive(Debug, Error)]
+pub enum ParseError {
+ #[error("OCI spec error: {0}")]
+ OciSpec(#[from] OciSpecError),
+ #[error("Wrong media type")]
+ WrongMediaType,
+ #[error("IO error: {0}")]
+ Io(#[from] std::io::Error),
+ #[error("Unsupported CPU architecture")]
+ UnsupportedArchitecture,
+ #[error("Missing image config")]
+ MissingImageConfig,
+}
+
+fn parse_image<P: AsRef<Path>>(
+ oci_tar_path: P,
+) -> Result<(OciTarImage<File>, ImageManifest, ImageConfiguration), ParseError> {
+ let oci_tar_file = File::open(oci_tar_path)?;
+ let mut oci_tar_image = OciTarImage::new(oci_tar_file)?;
+
+ let image_manifest = oci_tar_image
+ .image_manifest(&Arch::Amd64)
+ .ok_or(ParseError::UnsupportedArchitecture)??;
+
+ let image_config_descriptor = image_manifest.config();
+
+ if image_config_descriptor.media_type() != &MediaType::ImageConfig {
+ return Err(ParseError::WrongMediaType);
+ }
+
+ let mut image_config_file = oci_tar_image
+ .open_blob(image_config_descriptor.digest())
+ .ok_or(ParseError::MissingImageConfig)?;
+ let image_config = ImageConfiguration::from_reader(&mut image_config_file)?;
+
+ Ok((
+ image_config_file.into_oci_tar_image(),
+ image_manifest,
+ image_config,
+ ))
+}
+
+#[derive(Debug, Error)]
+pub enum ExtractError {
+ #[error("Incorrectly formatted digest: \"{0}\"")]
+ InvalidDigest(String),
+ #[error("Unknown layer digest {0} found in rootfs.diff_ids")]
+ UnknownLayerDigest(oci_spec::image::Digest),
+ #[error("Layer file {0} mentioned in image manifest is missing")]
+ MissingLayerFile(oci_spec::image::Digest),
+ #[error("IO error: {0}")]
+ Io(#[from] std::io::Error),
+ #[error("Layer has wrong media type: {0}")]
+ WrongMediaType(String),
+}
+
+fn extract_image_rootfs<R: Read + Seek, P: AsRef<Path>>(
+ oci_tar_image: OciTarImage<R>,
+ image_manifest: &ImageManifest,
+ image_config: &ImageConfiguration,
+ target_path: P,
+) -> Result<(), ExtractError> {
+ let (mut oci_tar_image, layer_map) = build_layer_map(oci_tar_image, image_manifest)?;
+
+ for layer in image_config.rootfs().diff_ids() {
+ let layer_digest = oci_spec::image::Digest::from_str(layer)
+ .map_err(|_| ExtractError::InvalidDigest(layer.to_string()))?;
+ let layer_descriptor = layer_map
+ .get(&layer_digest)
+ .ok_or(ExtractError::UnknownLayerDigest(layer_digest.clone()))?;
+ let mut layer_file = oci_tar_image
+ .open_blob(layer_descriptor.digest())
+ .ok_or(ExtractError::MissingLayerFile(layer_digest))?;
+
+ let (whiteouts, opaque_whiteouts) = match layer_descriptor.media_type() {
+ MediaType::ImageLayer | MediaType::ImageLayerNonDistributable => {
+ extract_archive(&mut layer_file, &target_path)?
+ }
+ MediaType::ImageLayerGzip | MediaType::ImageLayerNonDistributableGzip => {
+ let mut gz_decoder = GzDecoder::new(&mut layer_file);
+ extract_archive(&mut gz_decoder, &target_path)?
+ }
+ MediaType::ImageLayerZstd | MediaType::ImageLayerNonDistributableZstd => {
+ let mut zstd_decoder = zstd::Decoder::new(&mut layer_file)?;
+ extract_archive(&mut zstd_decoder, &target_path)?
+ }
+ // Error on any other non-ImageLayer related media types.
+ // Match explicitly to avoid missing new image layer types when oci-spec updates.
+ media_type @ (MediaType::Descriptor
+ | MediaType::LayoutHeader
+ | MediaType::ImageManifest
+ | MediaType::ImageIndex
+ | MediaType::ImageConfig
+ | MediaType::ArtifactManifest
+ | MediaType::EmptyJSON
+ | MediaType::Other(_)) => {
+ return Err(ExtractError::WrongMediaType(media_type.to_string()))
+ }
+ };
+
+ oci_tar_image = layer_file.into_oci_tar_image();
+
+ for whiteout in whiteouts {
+ let wh_abs_path = target_path.as_ref().join(&whiteout);
+ remove_path(wh_abs_path)?;
+ }
+
+ for opaque_whiteout in opaque_whiteouts {
+ let wh_abs_path = target_path.as_ref().join(&opaque_whiteout);
+ for direntry in read_dir(wh_abs_path)? {
+ remove_path(direntry?.path())?;
+ }
+ }
+ }
+
+ Ok(())
+}
+
+fn extract_archive<R: Read, P: AsRef<Path>>(
+ reader: &mut R,
+ target_path: P,
+) -> std::io::Result<(Vec<PathBuf>, Vec<PathBuf>)> {
+ const WHITEOUT_PREFIX: &str = ".wh.";
+ const OPAQUE_WHITEOUT_NAME: &str = ".wh..wh..opq";
+
+ let mut archive = Archive::new(reader);
+ archive.set_preserve_ownerships(true);
+ archive.set_preserve_permissions(true);
+ archive.set_unpack_xattrs(true);
+ let mut directories = Vec::new();
+ let mut whiteouts = Vec::new();
+ let mut opaque_whiteouts = Vec::new();
+
+ for entry in archive.entries()? {
+ let mut file = entry?;
+ if file.header().entry_type() == EntryType::Directory {
+ directories.push(file);
+ } else {
+ let filepath = file.path()?.into_owned();
+ if let Some(filename) = filepath.file_name() {
+ if filename == OPAQUE_WHITEOUT_NAME {
+ if let Some(parent) = filepath.parent() {
+ opaque_whiteouts.push(parent.to_path_buf());
+ }
+ continue;
+ } else if let Some(filename) = filename.to_str() {
+ if let Some(filename_stripped) = filename.strip_prefix(WHITEOUT_PREFIX) {
+ whiteouts.push(filepath.with_file_name(filename_stripped));
+ continue;
+ }
+ }
+ }
+
+ file.unpack_in(&target_path)?;
+ }
+ }
+
+ directories.sort_by(|a, b| b.path_bytes().cmp(&a.path_bytes()));
+ for mut dir in directories {
+ dir.unpack_in(&target_path)?;
+ }
+
+ Ok((whiteouts, opaque_whiteouts))
+}
+
+fn remove_path(path: PathBuf) -> std::io::Result<()> {
+ if path.metadata()?.is_dir() {
+ remove_dir_all(path)
+ } else {
+ remove_file(path)
+ }
+}
diff --git a/proxmox-oci/src/oci_tar_image.rs b/proxmox-oci/src/oci_tar_image.rs
new file mode 100644
index 00000000..212f6b53
--- /dev/null
+++ b/proxmox-oci/src/oci_tar_image.rs
@@ -0,0 +1,145 @@
+use std::collections::HashMap;
+use std::io::{Read, Seek, SeekFrom};
+use std::ops::Range;
+use std::path::{Path, PathBuf};
+
+use oci_spec::image::{Arch, Digest, ImageIndex, ImageManifest, MediaType};
+use oci_spec::OciSpecError;
+use tar::Archive;
+
+use proxmox_io::RangeReader;
+
+#[derive(Clone)]
+struct TarEntry {
+ range: Range<u64>,
+}
+
+impl TarEntry {
+ fn new(range: Range<u64>) -> Self {
+ Self { range }
+ }
+}
+
+pub struct OciTarImage<R: Read + Seek> {
+ reader: R,
+ entries: HashMap<PathBuf, TarEntry>,
+ image_index: ImageIndex,
+}
+
+impl<R: Read + Seek> OciTarImage<R> {
+ pub fn new(reader: R) -> oci_spec::Result<Self> {
+ let mut archive = Archive::new(reader);
+ let entries = archive.entries_with_seek()?;
+ let mut entries_index = HashMap::new();
+ let mut image_index = None;
+
+ for entry in entries {
+ let mut entry = entry?;
+ let offset = entry.raw_file_position();
+ let size = entry.size();
+ let path = entry.path()?.into_owned();
+
+ if path.as_path() == Path::new("index.json") {
+ image_index = Some(ImageIndex::from_reader(&mut entry)?);
+ }
+
+ let tar_entry = TarEntry::new(offset..(offset + size));
+ entries_index.insert(path, tar_entry);
+ }
+
+ if let Some(image_index) = image_index {
+ Ok(Self {
+ reader: archive.into_inner(),
+ entries: entries_index,
+ image_index,
+ })
+ } else {
+ Err(OciSpecError::Other("Missing index.json file".into()))
+ }
+ }
+
+ pub fn image_index(&self) -> &ImageIndex {
+ &self.image_index
+ }
+
+ fn get_blob_entry(&self, digest: &Digest) -> Option<TarEntry> {
+ let path = get_blob_path(digest);
+ self.entries.get(&path).cloned()
+ }
+
+ pub fn open_blob(self, digest: &Digest) -> Option<OciTarImageBlob<R>> {
+ if let Some(entry) = self.get_blob_entry(digest) {
+ Some(OciTarImageBlob::new(self, entry.range))
+ } else {
+ None
+ }
+ }
+
+ pub fn image_manifest(
+ &mut self,
+ architecture: &Arch,
+ ) -> Option<oci_spec::Result<ImageManifest>> {
+ let digest = match self.image_index.manifests().iter().find(|&x| {
+ x.media_type() == &MediaType::ImageManifest
+ && x.platform()
+ .as_ref()
+ .is_none_or(|platform| platform.architecture() == architecture)
+ }) {
+ Some(descriptor) => descriptor.digest(),
+ None => return None,
+ };
+
+ if let Some(entry) = self.get_blob_entry(digest) {
+ let mut range_reader = RangeReader::new(&mut self.reader, entry.range);
+ Some(ImageManifest::from_reader(&mut range_reader))
+ } else {
+ Some(Err(OciSpecError::Other(format!(
+ "Image manifest with digest {digest} mentioned in image index is missing"
+ ))))
+ }
+ }
+}
+
+fn get_blob_path(digest: &Digest) -> PathBuf {
+ let algorithm = digest.algorithm();
+ let digest = digest.digest();
+ format!("blobs/{algorithm}/{digest}").into()
+}
+
+pub struct OciTarImageBlob<R: Read + Seek> {
+ range_reader: RangeReader<R>,
+ entries: HashMap<PathBuf, TarEntry>,
+ image_index: ImageIndex,
+}
+
+impl<R: Read + Seek> OciTarImageBlob<R> {
+ fn new(archive: OciTarImage<R>, range: Range<u64>) -> Self {
+ let range_reader = RangeReader::new(archive.reader, range);
+
+ Self {
+ range_reader,
+ entries: archive.entries,
+ image_index: archive.image_index,
+ }
+ }
+
+ pub fn into_oci_tar_image(self) -> OciTarImage<R> {
+ OciTarImage {
+ reader: self.range_reader.into_inner(),
+ entries: self.entries,
+ image_index: self.image_index,
+ }
+ }
+}
+
+impl<R: Read + Seek> Read for OciTarImageBlob<R> {
+ fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
+ self.range_reader.read(buf)
+ }
+}
+
+impl<R: Read + Seek> Seek for OciTarImageBlob<R> {
+ fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
+ self.range_reader.seek(pos)
+ }
+}
--
2.47.2
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 21+ messages in thread
* Re: [pve-devel] [PATCH proxmox v3 02/13] add proxmox-oci crate
2025-07-09 12:34 ` [pve-devel] [PATCH proxmox v3 02/13] add proxmox-oci crate Filip Schauer
@ 2025-07-10 8:46 ` Wolfgang Bumiller
0 siblings, 0 replies; 21+ messages in thread
From: Wolfgang Bumiller @ 2025-07-10 8:46 UTC (permalink / raw)
To: Filip Schauer; +Cc: pve-devel
On Wed, Jul 09, 2025 at 02:34:19PM +0200, Filip Schauer wrote:
> This crate can parse an OCI image tarball and extract its rootfs. Layers
> are applied in sequence, but an overlay filesystem is currently not
> used.
>
> Signed-off-by: Filip Schauer <f.schauer@proxmox.com>
> ---
> Changed since v2:
> * remove reachable unwraps & refactor code
> * increase hasher buffer size from 4096 to 32768 (matching internal
> sha2::Digest buffering)
> * preserve permissions and xattrs during rootfs extraction
> * handle whiteouts & opaque whiteouts
>
> Cargo.toml | 1 +
> proxmox-oci/Cargo.toml | 22 +++
> proxmox-oci/debian/changelog | 5 +
> proxmox-oci/debian/control | 45 +++++
> proxmox-oci/debian/debcargo.toml | 7 +
> proxmox-oci/src/lib.rs | 283 +++++++++++++++++++++++++++++++
> proxmox-oci/src/oci_tar_image.rs | 145 ++++++++++++++++
> 7 files changed, 508 insertions(+)
> create mode 100644 proxmox-oci/Cargo.toml
> create mode 100644 proxmox-oci/debian/changelog
> create mode 100644 proxmox-oci/debian/control
> create mode 100644 proxmox-oci/debian/debcargo.toml
> create mode 100644 proxmox-oci/src/lib.rs
> create mode 100644 proxmox-oci/src/oci_tar_image.rs
>
> diff --git a/Cargo.toml b/Cargo.toml
> index 020e7497..4606fc19 100644
> --- a/Cargo.toml
> +++ b/Cargo.toml
> @@ -26,6 +26,7 @@ members = [
> "proxmox-metrics",
> "proxmox-network-api",
> "proxmox-notify",
> + "proxmox-oci",
> "proxmox-openid",
> "proxmox-product-config",
> "proxmox-resource-scheduling",
> diff --git a/proxmox-oci/Cargo.toml b/proxmox-oci/Cargo.toml
> new file mode 100644
> index 00000000..4daff6ab
> --- /dev/null
> +++ b/proxmox-oci/Cargo.toml
> @@ -0,0 +1,22 @@
> +[package]
> +name = "proxmox-oci"
> +description = "OCI image parsing and extraction"
> +version = "0.1.0"
> +
> +authors.workspace = true
> +edition.workspace = true
> +exclude.workspace = true
> +homepage.workspace = true
> +license.workspace = true
> +repository.workspace = true
> +rust-version.workspace = true
> +
> +[dependencies]
> +flate2.workspace = true
> +oci-spec = "0.8.1"
> +sha2 = "0.10"
> +tar.workspace = true
> +thiserror = "1"
> +zstd.workspace = true
> +
> +proxmox-io.workspace = true
> diff --git a/proxmox-oci/debian/changelog b/proxmox-oci/debian/changelog
> new file mode 100644
> index 00000000..754d06c1
> --- /dev/null
> +++ b/proxmox-oci/debian/changelog
> @@ -0,0 +1,5 @@
> +rust-proxmox-oci (0.1.0-1) bookworm; urgency=medium
> +
> + * Initial release.
> +
> + -- Proxmox Support Team <support@proxmox.com> Mon, 28 Apr 2025 12:34:56 +0200
> diff --git a/proxmox-oci/debian/control b/proxmox-oci/debian/control
> new file mode 100644
> index 00000000..f33331c5
> --- /dev/null
> +++ b/proxmox-oci/debian/control
> @@ -0,0 +1,45 @@
> +Source: rust-proxmox-oci
> +Section: rust
> +Priority: optional
> +Build-Depends: debhelper-compat (= 13),
> + dh-sequence-cargo
> +Build-Depends-Arch: cargo:native <!nocheck>,
> + rustc:native (>= 1.82) <!nocheck>,
> + libstd-rust-dev <!nocheck>,
> + librust-flate2-1+default-dev <!nocheck>,
> + librust-oci-spec-0.8+default-dev (>= 0.8.1-~~) <!nocheck>,
> + librust-proxmox-io-1+default-dev (>= 1.2.0-~~) <!nocheck>,
> + librust-sha2-0.10+default-dev <!nocheck>,
> + librust-tar-0.4+default-dev <!nocheck>,
> + librust-thiserror-1+default-dev <!nocheck>,
> + librust-zstd-0.13+default-dev <!nocheck>
> +Maintainer: Proxmox Support Team <support@proxmox.com>
> +Standards-Version: 4.7.0
> +Vcs-Git: git://git.proxmox.com/git/proxmox.git
> +Vcs-Browser: https://git.proxmox.com/?p=proxmox.git
> +Homepage: https://proxmox.com
> +X-Cargo-Crate: proxmox-oci
> +Rules-Requires-Root: no
> +
> +Package: librust-proxmox-oci-dev
> +Architecture: any
> +Multi-Arch: same
> +Depends:
> + ${misc:Depends},
> + librust-flate2-1+default-dev,
> + librust-oci-spec-0.8+default-dev (>= 0.8.1-~~),
> + librust-proxmox-io-1+default-dev (>= 1.2.0-~~),
> + librust-sha2-0.10+default-dev,
> + librust-tar-0.4+default-dev,
> + librust-thiserror-1+default-dev,
> + librust-zstd-0.13+default-dev
> +Provides:
> + librust-proxmox-oci+default-dev (= ${binary:Version}),
> + librust-proxmox-oci-0-dev (= ${binary:Version}),
> + librust-proxmox-oci-0+default-dev (= ${binary:Version}),
> + librust-proxmox-oci-0.1-dev (= ${binary:Version}),
> + librust-proxmox-oci-0.1+default-dev (= ${binary:Version}),
> + librust-proxmox-oci-0.1.0-dev (= ${binary:Version}),
> + librust-proxmox-oci-0.1.0+default-dev (= ${binary:Version})
> +Description: OCI image parsing and extraction - Rust source code
> + Source code for Debianized Rust crate "proxmox-oci"
> diff --git a/proxmox-oci/debian/debcargo.toml b/proxmox-oci/debian/debcargo.toml
> new file mode 100644
> index 00000000..b7864cdb
> --- /dev/null
> +++ b/proxmox-oci/debian/debcargo.toml
> @@ -0,0 +1,7 @@
> +overlay = "."
> +crate_src_path = ".."
> +maintainer = "Proxmox Support Team <support@proxmox.com>"
> +
> +[source]
> +vcs_git = "git://git.proxmox.com/git/proxmox.git"
> +vcs_browser = "https://git.proxmox.com/?p=proxmox.git"
> diff --git a/proxmox-oci/src/lib.rs b/proxmox-oci/src/lib.rs
> new file mode 100644
> index 00000000..b54d06e1
> --- /dev/null
> +++ b/proxmox-oci/src/lib.rs
> @@ -0,0 +1,283 @@
> +use std::collections::HashMap;
> +use std::fs::{read_dir, remove_dir_all, remove_file, File};
> +use std::io::{Read, Seek};
> +use std::path::{Path, PathBuf};
> +use std::str::FromStr;
> +
> +use flate2::read::GzDecoder;
> +pub use oci_spec::image::Config;
> +use oci_spec::image::{Arch, ImageConfiguration, ImageManifest, MediaType};
> +use oci_spec::OciSpecError;
> +use sha2::digest::generic_array::GenericArray;
> +use sha2::{Digest, Sha256};
> +use tar::{Archive, EntryType};
> +use thiserror::Error;
> +
> +mod oci_tar_image;
> +use oci_tar_image::OciTarImage;
> +
> +fn compute_digest<R: Read, H: Digest>(
> + mut reader: R,
> + mut hasher: H,
> +) -> std::io::Result<GenericArray<u8, H::OutputSize>> {
> + let mut buf = proxmox_io::boxed::zeroed(32768);
> +
> + loop {
> + let bytes_read = reader.read(&mut buf)?;
> + if bytes_read == 0 {
> + break Ok(hasher.finalize());
> + }
> +
> + hasher.update(&buf[..bytes_read]);
> + }
> +}
> +
> +fn compute_sha256<R: Read>(reader: R) -> std::io::Result<oci_spec::image::Sha256Digest> {
> + let digest = compute_digest(reader, Sha256::new())?;
> + Ok(oci_spec::image::Sha256Digest::from_str(&format!("{digest:x}")).unwrap())
> +}
> +
> +/// Build a mapping from uncompressed layer digests (as found in the image config's `rootfs.diff_ids`)
> +/// to their corresponding compressed-layer digests (i.e. the filenames under `blobs/<algorithm>/<digest>`)
> +fn build_layer_map<R: Read + Seek>(
> + mut oci_tar_image: OciTarImage<R>,
> + image_manifest: &ImageManifest,
> +) -> Result<
> + (
> + OciTarImage<R>,
> + HashMap<oci_spec::image::Digest, oci_spec::image::Descriptor>,
> + ),
> + ExtractError,
> +> {
> + let mut layer_mapping = HashMap::new();
> +
> + for layer in image_manifest.layers() {
> + let digest = match layer.media_type() {
> + MediaType::ImageLayer | MediaType::ImageLayerNonDistributable => layer.digest().clone(),
> + MediaType::ImageLayerGzip | MediaType::ImageLayerNonDistributableGzip => {
> + let mut compressed_blob = oci_tar_image
> + .open_blob(layer.digest())
> + .ok_or(ExtractError::MissingLayerFile(layer.digest().clone()))?;
> + let decoder = GzDecoder::new(&mut compressed_blob);
> + let hash = compute_sha256(decoder)?.into();
> + oci_tar_image = compressed_blob.into_oci_tar_image();
> + hash
> + }
> + MediaType::ImageLayerZstd | MediaType::ImageLayerNonDistributableZstd => {
> + let mut compressed_blob = oci_tar_image
> + .open_blob(layer.digest())
> + .ok_or(ExtractError::MissingLayerFile(layer.digest().clone()))?;
> + let decoder = zstd::Decoder::new(&mut compressed_blob)?;
> + let hash = compute_sha256(decoder)?.into();
> + oci_tar_image = compressed_blob.into_oci_tar_image();
> + hash
> + }
> + // Skip any other non-ImageLayer related media types.
> + // Match explicitly to avoid missing new image layer types when oci-spec updates.
> + MediaType::Descriptor
> + | MediaType::LayoutHeader
> + | MediaType::ImageManifest
> + | MediaType::ImageIndex
> + | MediaType::ImageConfig
> + | MediaType::ArtifactManifest
> + | MediaType::EmptyJSON
> + | MediaType::Other(_) => continue,
> + };
> +
> + layer_mapping.insert(digest, layer.clone());
> + }
> +
> + Ok((oci_tar_image, layer_mapping))
> +}
> +
> +#[derive(Debug, Error)]
> +pub enum ProxmoxOciError {
> + #[error("Error while parsing OCI image: {0}")]
> + ParseError(#[from] ParseError),
> + #[error("Error while extracting OCI image: {0}")]
> + ExtractError(#[from] ExtractError),
> +}
> +
> +pub fn parse_and_extract_image<P: AsRef<Path>>(
> + oci_tar_path: P,
> + rootfs_path: P,
> +) -> Result<Option<Config>, ProxmoxOciError> {
> + let (oci_tar_image, image_manifest, image_config) = parse_image(oci_tar_path)?;
> +
> + extract_image_rootfs(oci_tar_image, &image_manifest, &image_config, rootfs_path)?;
> +
> + Ok(image_config.config().clone())
> +}
> +
> +#[derive(Debug, Error)]
> +pub enum ParseError {
> + #[error("OCI spec error: {0}")]
> + OciSpec(#[from] OciSpecError),
> + #[error("Wrong media type")]
> + WrongMediaType,
> + #[error("IO error: {0}")]
> + Io(#[from] std::io::Error),
> + #[error("Unsupported CPU architecture")]
> + UnsupportedArchitecture,
> + #[error("Missing image config")]
> + MissingImageConfig,
> +}
> +
> +fn parse_image<P: AsRef<Path>>(
> + oci_tar_path: P,
> +) -> Result<(OciTarImage<File>, ImageManifest, ImageConfiguration), ParseError> {
> + let oci_tar_file = File::open(oci_tar_path)?;
> + let mut oci_tar_image = OciTarImage::new(oci_tar_file)?;
> +
> + let image_manifest = oci_tar_image
> + .image_manifest(&Arch::Amd64)
> + .ok_or(ParseError::UnsupportedArchitecture)??;
> +
> + let image_config_descriptor = image_manifest.config();
> +
> + if image_config_descriptor.media_type() != &MediaType::ImageConfig {
> + return Err(ParseError::WrongMediaType);
> + }
> +
> + let mut image_config_file = oci_tar_image
> + .open_blob(image_config_descriptor.digest())
> + .ok_or(ParseError::MissingImageConfig)?;
> + let image_config = ImageConfiguration::from_reader(&mut image_config_file)?;
> +
> + Ok((
> + image_config_file.into_oci_tar_image(),
> + image_manifest,
> + image_config,
> + ))
> +}
> +
> +#[derive(Debug, Error)]
> +pub enum ExtractError {
> + #[error("Incorrectly formatted digest: \"{0}\"")]
> + InvalidDigest(String),
> + #[error("Unknown layer digest {0} found in rootfs.diff_ids")]
> + UnknownLayerDigest(oci_spec::image::Digest),
> + #[error("Layer file {0} mentioned in image manifest is missing")]
> + MissingLayerFile(oci_spec::image::Digest),
> + #[error("IO error: {0}")]
> + Io(#[from] std::io::Error),
> + #[error("Layer has wrong media type: {0}")]
> + WrongMediaType(String),
> +}
> +
> +fn extract_image_rootfs<R: Read + Seek, P: AsRef<Path>>(
> + oci_tar_image: OciTarImage<R>,
> + image_manifest: &ImageManifest,
> + image_config: &ImageConfiguration,
> + target_path: P,
> +) -> Result<(), ExtractError> {
> + let (mut oci_tar_image, layer_map) = build_layer_map(oci_tar_image, image_manifest)?;
> +
> + for layer in image_config.rootfs().diff_ids() {
> + let layer_digest = oci_spec::image::Digest::from_str(layer)
> + .map_err(|_| ExtractError::InvalidDigest(layer.to_string()))?;
> + let layer_descriptor = layer_map
> + .get(&layer_digest)
> + .ok_or(ExtractError::UnknownLayerDigest(layer_digest.clone()))?;
> + let mut layer_file = oci_tar_image
> + .open_blob(layer_descriptor.digest())
> + .ok_or(ExtractError::MissingLayerFile(layer_digest))?;
> +
> + let (whiteouts, opaque_whiteouts) = match layer_descriptor.media_type() {
> + MediaType::ImageLayer | MediaType::ImageLayerNonDistributable => {
> + extract_archive(&mut layer_file, &target_path)?
> + }
> + MediaType::ImageLayerGzip | MediaType::ImageLayerNonDistributableGzip => {
> + let mut gz_decoder = GzDecoder::new(&mut layer_file);
> + extract_archive(&mut gz_decoder, &target_path)?
> + }
> + MediaType::ImageLayerZstd | MediaType::ImageLayerNonDistributableZstd => {
> + let mut zstd_decoder = zstd::Decoder::new(&mut layer_file)?;
> + extract_archive(&mut zstd_decoder, &target_path)?
> + }
> + // Error on any other non-ImageLayer related media types.
> + // Match explicitly to avoid missing new image layer types when oci-spec updates.
> + media_type @ (MediaType::Descriptor
> + | MediaType::LayoutHeader
> + | MediaType::ImageManifest
> + | MediaType::ImageIndex
> + | MediaType::ImageConfig
> + | MediaType::ArtifactManifest
> + | MediaType::EmptyJSON
> + | MediaType::Other(_)) => {
> + return Err(ExtractError::WrongMediaType(media_type.to_string()))
> + }
> + };
> +
> + oci_tar_image = layer_file.into_oci_tar_image();
> +
> + for whiteout in whiteouts {
> + let wh_abs_path = target_path.as_ref().join(&whiteout);
> + remove_path(wh_abs_path)?;
> + }
> +
> + for opaque_whiteout in opaque_whiteouts {
> + let wh_abs_path = target_path.as_ref().join(&opaque_whiteout);
> + for direntry in read_dir(wh_abs_path)? {
> + remove_path(direntry?.path())?;
> + }
> + }
> + }
> +
> + Ok(())
> +}
> +
> +fn extract_archive<R: Read, P: AsRef<Path>>(
> + reader: &mut R,
> + target_path: P,
> +) -> std::io::Result<(Vec<PathBuf>, Vec<PathBuf>)> {
> + const WHITEOUT_PREFIX: &str = ".wh.";
> + const OPAQUE_WHITEOUT_NAME: &str = ".wh..wh..opq";
> +
> + let mut archive = Archive::new(reader);
> + archive.set_preserve_ownerships(true);
> + archive.set_preserve_permissions(true);
> + archive.set_unpack_xattrs(true);
> + let mut directories = Vec::new();
> + let mut whiteouts = Vec::new();
> + let mut opaque_whiteouts = Vec::new();
> +
> + for entry in archive.entries()? {
> + let mut file = entry?;
> + if file.header().entry_type() == EntryType::Directory {
> + directories.push(file);
> + } else {
> + let filepath = file.path()?.into_owned();
> + if let Some(filename) = filepath.file_name() {
> + if filename == OPAQUE_WHITEOUT_NAME {
> + if let Some(parent) = filepath.parent() {
> + opaque_whiteouts.push(parent.to_path_buf());
Paths can *technically* exist as both a whiteout and a new entry, so
delaying the removal to after extraction may potentially remove files
which should still exist.
*Technically* the spec allows whiteouts to be ordered "wrong"...
The spec states:
- Whiteout files MUST only apply to resources in lower/parent layers.
- Files that are present in the same layer as a whiteout file can only be hidden by whiteout files in subsequent layers.
So in order to be "truly" correct, we'd have to go through the archive
twice: once to apply all the whiteouts, and then to extract all the
non-whiteouts...
I'm not sure this happens in the real world, but I think code-wise it's
easy enough.
> + }
> + continue;
> + } else if let Some(filename) = filename.to_str() {
> + if let Some(filename_stripped) = filename.strip_prefix(WHITEOUT_PREFIX) {
> + whiteouts.push(filepath.with_file_name(filename_stripped));
> + continue;
> + }
> + }
> + }
> +
At this point we also have to remove the destination - potentially
recursively. When replacing a directory with a file, the new layer
simply contains the file without any previous whiteouts.
> + file.unpack_in(&target_path)?;
> + }
> + }
> +
> + directories.sort_by(|a, b| b.path_bytes().cmp(&a.path_bytes()));
> + for mut dir in directories {
> + dir.unpack_in(&target_path)?;
> + }
> +
> + Ok((whiteouts, opaque_whiteouts))
> +}
> +
> +fn remove_path(path: PathBuf) -> std::io::Result<()> {
> + if path.metadata()?.is_dir() {
> + remove_dir_all(path)
> + } else {
> + remove_file(path)
> + }
> +}
> diff --git a/proxmox-oci/src/oci_tar_image.rs b/proxmox-oci/src/oci_tar_image.rs
> new file mode 100644
> index 00000000..212f6b53
> --- /dev/null
> +++ b/proxmox-oci/src/oci_tar_image.rs
> @@ -0,0 +1,145 @@
> +use std::collections::HashMap;
> +use std::io::{Read, Seek, SeekFrom};
> +use std::ops::Range;
> +use std::path::{Path, PathBuf};
> +
> +use oci_spec::image::{Arch, Digest, ImageIndex, ImageManifest, MediaType};
> +use oci_spec::OciSpecError;
> +use tar::Archive;
> +
> +use proxmox_io::RangeReader;
> +
> +#[derive(Clone)]
> +struct TarEntry {
> + range: Range<u64>,
> +}
> +
> +impl TarEntry {
> + fn new(range: Range<u64>) -> Self {
> + Self { range }
> + }
> +}
> +
> +pub struct OciTarImage<R: Read + Seek> {
> + reader: R,
> + entries: HashMap<PathBuf, TarEntry>,
> + image_index: ImageIndex,
> +}
> +
> +impl<R: Read + Seek> OciTarImage<R> {
> + pub fn new(reader: R) -> oci_spec::Result<Self> {
> + let mut archive = Archive::new(reader);
> + let entries = archive.entries_with_seek()?;
> + let mut entries_index = HashMap::new();
> + let mut image_index = None;
> +
> + for entry in entries {
> + let mut entry = entry?;
> + let offset = entry.raw_file_position();
> + let size = entry.size();
> + let path = entry.path()?.into_owned();
> +
> + if path.as_path() == Path::new("index.json") {
> + image_index = Some(ImageIndex::from_reader(&mut entry)?);
> + }
> +
> + let tar_entry = TarEntry::new(offset..(offset + size));
> + entries_index.insert(path, tar_entry);
> + }
> +
> + if let Some(image_index) = image_index {
> + Ok(Self {
> + reader: archive.into_inner(),
> + entries: entries_index,
> + image_index,
> + })
> + } else {
> + Err(OciSpecError::Other("Missing index.json file".into()))
> + }
> + }
> +
> + pub fn image_index(&self) -> &ImageIndex {
> + &self.image_index
> + }
> +
> + fn get_blob_entry(&self, digest: &Digest) -> Option<TarEntry> {
> + let path = get_blob_path(digest);
> + self.entries.get(&path).cloned()
> + }
> +
> + pub fn open_blob(self, digest: &Digest) -> Option<OciTarImageBlob<R>> {
> + if let Some(entry) = self.get_blob_entry(digest) {
> + Some(OciTarImageBlob::new(self, entry.range))
> + } else {
> + None
> + }
> + }
> +
> + pub fn image_manifest(
> + &mut self,
> + architecture: &Arch,
> + ) -> Option<oci_spec::Result<ImageManifest>> {
> + let digest = match self.image_index.manifests().iter().find(|&x| {
> + x.media_type() == &MediaType::ImageManifest
> + && x.platform()
> + .as_ref()
> + .is_none_or(|platform| platform.architecture() == architecture)
> + }) {
> + Some(descriptor) => descriptor.digest(),
> + None => return None,
> + };
> +
> + if let Some(entry) = self.get_blob_entry(digest) {
> + let mut range_reader = RangeReader::new(&mut self.reader, entry.range);
> + Some(ImageManifest::from_reader(&mut range_reader))
> + } else {
> + Some(Err(OciSpecError::Other(format!(
> + "Image manifest with digest {digest} mentioned in image index is missing"
> + ))))
> + }
> + }
> +}
> +
> +fn get_blob_path(digest: &Digest) -> PathBuf {
> + let algorithm = digest.algorithm();
> + let digest = digest.digest();
> + format!("blobs/{algorithm}/{digest}").into()
> +}
> +
> +pub struct OciTarImageBlob<R: Read + Seek> {
> + range_reader: RangeReader<R>,
> + entries: HashMap<PathBuf, TarEntry>,
> + image_index: ImageIndex,
> +}
> +
> +impl<R: Read + Seek> OciTarImageBlob<R> {
> + fn new(archive: OciTarImage<R>, range: Range<u64>) -> Self {
> + let range_reader = RangeReader::new(archive.reader, range);
> +
> + Self {
> + range_reader,
> + entries: archive.entries,
> + image_index: archive.image_index,
> + }
> + }
> +
> + pub fn into_oci_tar_image(self) -> OciTarImage<R> {
> + OciTarImage {
> + reader: self.range_reader.into_inner(),
> + entries: self.entries,
> + image_index: self.image_index,
> + }
> + }
> +}
> +
> +impl<R: Read + Seek> Read for OciTarImageBlob<R> {
> + fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
> + self.range_reader.read(buf)
> + }
> +}
> +
> +impl<R: Read + Seek> Seek for OciTarImageBlob<R> {
> + fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
> + self.range_reader.seek(pos)
> + }
> +}
> --
> 2.47.2
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 21+ messages in thread
* [pve-devel] [PATCH proxmox-perl-rs v3 03/13] add Perl mapping for OCI container image parser/extractor
2025-07-09 12:34 [pve-devel] [PATCH container/docs/lxcfs/manager/proxmox{, -perl-rs}/storage v3 00/13] support OCI images as container templates Filip Schauer
2025-07-09 12:34 ` [pve-devel] [PATCH proxmox v3 01/13] io: introduce RangeReader for bounded reads Filip Schauer
2025-07-09 12:34 ` [pve-devel] [PATCH proxmox v3 02/13] add proxmox-oci crate Filip Schauer
@ 2025-07-09 12:34 ` Filip Schauer
2025-07-10 10:39 ` Wolfgang Bumiller
2025-07-09 12:34 ` [pve-devel] [PATCH container v3 04/13] add support for OCI images as container templates Filip Schauer
` (9 subsequent siblings)
12 siblings, 1 reply; 21+ messages in thread
From: Filip Schauer @ 2025-07-09 12:34 UTC (permalink / raw)
To: pve-devel
Signed-off-by: Filip Schauer <f.schauer@proxmox.com>
---
This patch depends on the proxmox-oci crate added in patch 02/13.
Changed since v2:
* rebase onto newest master (6132d4d36cbd)
* forward all errors to Perl
* remove oci-spec dependency
Changed since v1:
* rebase on latest master (3d9806cb3c7f)
* add new dependencies to debian/control
pve-rs/Cargo.toml | 1 +
pve-rs/Makefile | 1 +
pve-rs/debian/control | 1 +
pve-rs/src/bindings/mod.rs | 3 +++
pve-rs/src/bindings/oci.rs | 21 +++++++++++++++++++++
5 files changed, 27 insertions(+)
create mode 100644 pve-rs/src/bindings/oci.rs
diff --git a/pve-rs/Cargo.toml b/pve-rs/Cargo.toml
index c7f11a3..4d38a5c 100644
--- a/pve-rs/Cargo.toml
+++ b/pve-rs/Cargo.toml
@@ -37,6 +37,7 @@ proxmox-http = { version = "1", features = ["client-sync", "client-trait"] }
proxmox-http-error = "1"
proxmox-log = "1"
proxmox-notify = { version = "1", features = ["pve-context"] }
+proxmox-oci = "0.1.0"
proxmox-openid = "1"
proxmox-resource-scheduling = "1"
proxmox-shared-cache = "1"
diff --git a/pve-rs/Makefile b/pve-rs/Makefile
index 773156a..d813766 100644
--- a/pve-rs/Makefile
+++ b/pve-rs/Makefile
@@ -27,6 +27,7 @@ PERLMOD_GENPACKAGE := /usr/lib/perlmod/genpackage.pl \
PERLMOD_PACKAGES := \
PVE::RS::Firewall::SDN \
+ PVE::RS::OCI \
PVE::RS::OpenId \
PVE::RS::ResourceScheduling::Static \
PVE::RS::TFA
diff --git a/pve-rs/debian/control b/pve-rs/debian/control
index 9e424ec..869ca50 100644
--- a/pve-rs/debian/control
+++ b/pve-rs/debian/control
@@ -24,6 +24,7 @@ Build-Depends: cargo:native <!nocheck>,
librust-proxmox-log-1+default-dev,
librust-proxmox-notify-1+default-dev (>= 0.5.4),
librust-proxmox-notify-1+pve-context-dev,
+ librust-proxmox-oci-0.1+default-dev,
librust-proxmox-openid-1+default-dev (>= 0.10.4-~~),
librust-proxmox-resource-scheduling-1+default-dev,
librust-proxmox-shared-cache-1+default-dev,
diff --git a/pve-rs/src/bindings/mod.rs b/pve-rs/src/bindings/mod.rs
index e4fb4db..17247f6 100644
--- a/pve-rs/src/bindings/mod.rs
+++ b/pve-rs/src/bindings/mod.rs
@@ -1,5 +1,8 @@
//! This contains all the perl bindings.
+mod oci;
+pub use oci::pve_rs_oci;
+
mod resource_scheduling_static;
pub use resource_scheduling_static::pve_rs_resource_scheduling_static;
diff --git a/pve-rs/src/bindings/oci.rs b/pve-rs/src/bindings/oci.rs
new file mode 100644
index 0000000..f7cfe41
--- /dev/null
+++ b/pve-rs/src/bindings/oci.rs
@@ -0,0 +1,21 @@
+#[perlmod::package(name = "PVE::RS::OCI")]
+pub mod pve_rs_oci {
+ //! The `PVE::RS::OCI` package.
+ //!
+ //! Provides bindings for the proxmox-oci crate.
+
+ use anyhow::Error;
+ use proxmox_oci::Config;
+
+ /// Method: Extract the rootfs of an OCI image tar and return the image config.
+ #[export]
+ pub fn parse_and_extract_image(
+ oci_tar_path: &str,
+ rootfs_path: &str,
+ ) -> Result<Option<Config>, Error> {
+ match proxmox_oci::parse_and_extract_image(oci_tar_path, rootfs_path) {
+ Ok(config) => Ok(Some(config.unwrap_or_default())),
+ Err(err) => Err(err.into()),
+ }
+ }
+}
--
2.47.2
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 21+ messages in thread
* Re: [pve-devel] [PATCH proxmox-perl-rs v3 03/13] add Perl mapping for OCI container image parser/extractor
2025-07-09 12:34 ` [pve-devel] [PATCH proxmox-perl-rs v3 03/13] add Perl mapping for OCI container image parser/extractor Filip Schauer
@ 2025-07-10 10:39 ` Wolfgang Bumiller
0 siblings, 0 replies; 21+ messages in thread
From: Wolfgang Bumiller @ 2025-07-10 10:39 UTC (permalink / raw)
To: Filip Schauer; +Cc: pve-devel
On Wed, Jul 09, 2025 at 02:34:20PM +0200, Filip Schauer wrote:
> Signed-off-by: Filip Schauer <f.schauer@proxmox.com>
> ---
> This patch depends on the proxmox-oci crate added in patch 02/13.
>
> Changed since v2:
> * rebase onto newest master (6132d4d36cbd)
> * forward all errors to Perl
> * remove oci-spec dependency
>
> Changed since v1:
> * rebase on latest master (3d9806cb3c7f)
> * add new dependencies to debian/control
>
> pve-rs/Cargo.toml | 1 +
> pve-rs/Makefile | 1 +
> pve-rs/debian/control | 1 +
> pve-rs/src/bindings/mod.rs | 3 +++
> pve-rs/src/bindings/oci.rs | 21 +++++++++++++++++++++
> 5 files changed, 27 insertions(+)
> create mode 100644 pve-rs/src/bindings/oci.rs
>
> diff --git a/pve-rs/Cargo.toml b/pve-rs/Cargo.toml
> index c7f11a3..4d38a5c 100644
> --- a/pve-rs/Cargo.toml
> +++ b/pve-rs/Cargo.toml
> @@ -37,6 +37,7 @@ proxmox-http = { version = "1", features = ["client-sync", "client-trait"] }
> proxmox-http-error = "1"
> proxmox-log = "1"
> proxmox-notify = { version = "1", features = ["pve-context"] }
> +proxmox-oci = "0.1.0"
> proxmox-openid = "1"
> proxmox-resource-scheduling = "1"
> proxmox-shared-cache = "1"
> diff --git a/pve-rs/Makefile b/pve-rs/Makefile
> index 773156a..d813766 100644
> --- a/pve-rs/Makefile
> +++ b/pve-rs/Makefile
> @@ -27,6 +27,7 @@ PERLMOD_GENPACKAGE := /usr/lib/perlmod/genpackage.pl \
>
> PERLMOD_PACKAGES := \
> PVE::RS::Firewall::SDN \
> + PVE::RS::OCI \
> PVE::RS::OpenId \
> PVE::RS::ResourceScheduling::Static \
> PVE::RS::TFA
> diff --git a/pve-rs/debian/control b/pve-rs/debian/control
> index 9e424ec..869ca50 100644
> --- a/pve-rs/debian/control
> +++ b/pve-rs/debian/control
> @@ -24,6 +24,7 @@ Build-Depends: cargo:native <!nocheck>,
> librust-proxmox-log-1+default-dev,
> librust-proxmox-notify-1+default-dev (>= 0.5.4),
> librust-proxmox-notify-1+pve-context-dev,
> + librust-proxmox-oci-0.1+default-dev,
> librust-proxmox-openid-1+default-dev (>= 0.10.4-~~),
> librust-proxmox-resource-scheduling-1+default-dev,
> librust-proxmox-shared-cache-1+default-dev,
> diff --git a/pve-rs/src/bindings/mod.rs b/pve-rs/src/bindings/mod.rs
> index e4fb4db..17247f6 100644
> --- a/pve-rs/src/bindings/mod.rs
> +++ b/pve-rs/src/bindings/mod.rs
> @@ -1,5 +1,8 @@
> //! This contains all the perl bindings.
>
> +mod oci;
> +pub use oci::pve_rs_oci;
> +
> mod resource_scheduling_static;
> pub use resource_scheduling_static::pve_rs_resource_scheduling_static;
>
> diff --git a/pve-rs/src/bindings/oci.rs b/pve-rs/src/bindings/oci.rs
> new file mode 100644
> index 0000000..f7cfe41
> --- /dev/null
> +++ b/pve-rs/src/bindings/oci.rs
> @@ -0,0 +1,21 @@
> +#[perlmod::package(name = "PVE::RS::OCI")]
> +pub mod pve_rs_oci {
> + //! The `PVE::RS::OCI` package.
> + //!
> + //! Provides bindings for the proxmox-oci crate.
[`proxmox-oci`]
> +
> + use anyhow::Error;
> + use proxmox_oci::Config;
> +
> + /// Method: Extract the rootfs of an OCI image tar and return the image config.
Not a method - just a regular function. (iow. called via
`PVE::RS::OCI::parse_and_extract_image`.
Methods would be `$an_instance->parse_and_extract_image(…)` and `Class
methods` would be `PVE::RS::OCI->parse_and_extract_image(…)`.
> + #[export]
> + pub fn parse_and_extract_image(
> + oci_tar_path: &str,
> + rootfs_path: &str,
> + ) -> Result<Option<Config>, Error> {
We don't seem to have a case where we return `Ok(None)` now?
> + match proxmox_oci::parse_and_extract_image(oci_tar_path, rootfs_path) {
> + Ok(config) => Ok(Some(config.unwrap_or_default())),
> + Err(err) => Err(err.into()),
> + }
> + }
> +}
> --
> 2.47.2
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 21+ messages in thread
* [pve-devel] [PATCH container v3 04/13] add support for OCI images as container templates
2025-07-09 12:34 [pve-devel] [PATCH container/docs/lxcfs/manager/proxmox{, -perl-rs}/storage v3 00/13] support OCI images as container templates Filip Schauer
` (2 preceding siblings ...)
2025-07-09 12:34 ` [pve-devel] [PATCH proxmox-perl-rs v3 03/13] add Perl mapping for OCI container image parser/extractor Filip Schauer
@ 2025-07-09 12:34 ` Filip Schauer
2025-07-10 10:31 ` Wolfgang Bumiller
2025-07-09 12:34 ` [pve-devel] [PATCH container v3 05/13] config: add entrypoint parameter Filip Schauer
` (8 subsequent siblings)
12 siblings, 1 reply; 21+ messages in thread
From: Filip Schauer @ 2025-07-09 12:34 UTC (permalink / raw)
To: pve-devel
This aims to add basic support for the Open Container Initiative image
format according to the specification. [0]
[0] https://github.com/opencontainers/image-spec/blob/main/spec.md
Signed-off-by: Filip Schauer <f.schauer@proxmox.com>
---
This patch depends on changes made to proxmox-perl-rs in patch 03/13.
Meaning that proxmox-perl-rs needs to be bumped and a dependency & build
dependency to libpve-rs-perl needs to be added to debian/control.
Changed since v2:
* rebase onto newest master (5a8b3f962f16) and re-format with
proxmox-perltidy
* check whether archive is an OCI image before trying to parse it as one
Changed since v1:
* fix entrypoint command missing Cmd
* set lxc.signal.halt according to StopSignal (Fixes container shutdown)
src/PVE/API2/LXC.pm | 96 ++++++++++++++++++++++++++++++++++++++++-----
1 file changed, 86 insertions(+), 10 deletions(-)
diff --git a/src/PVE/API2/LXC.pm b/src/PVE/API2/LXC.pm
index 28f7fdd..45c5cef 100644
--- a/src/PVE/API2/LXC.pm
+++ b/src/PVE/API2/LXC.pm
@@ -19,9 +19,11 @@ use PVE::Storage;
use PVE::RESTHandler;
use PVE::RPCEnvironment;
use PVE::ReplicationConfig;
+use PVE::RS::OCI;
use PVE::LXC;
use PVE::LXC::Create;
use PVE::LXC::Migrate;
+use PVE::LXC::Namespaces;
use PVE::GuestHelpers;
use PVE::VZDump::Plugin;
use PVE::API2::LXC::Config;
@@ -523,19 +525,93 @@ __PACKAGE__->register_method({
eval {
my $rootdir = PVE::LXC::mount_all($vmid, $storage_cfg, $conf, 1);
+ my $archivepath = PVE::Storage::abs_filesystem_path($storage_cfg, $archive);
$bwlimit = PVE::Storage::get_bandwidth_limit(
'restore', [keys %used_storages], $bwlimit,
);
- print "restoring '$archive' now..\n"
- if $restore && $archive ne '-';
- PVE::LXC::Create::restore_archive(
- $storage_cfg,
- $archive,
- $rootdir,
- $conf,
- $ignore_unpack_errors,
- $bwlimit,
- );
+ my $is_oci = 0;
+
+ if ($restore && $archive ne '-') {
+ print "restoring '$archive' now..\n";
+ } elsif ($archivepath =~ /\.tar$/) {
+ # Check whether archive is an OCI image
+ my $has_oci_layout = 0;
+ my $has_index_json = 0;
+ my $has_blobs = 0;
+ PVE::Tools::run_command(
+ ['tar', '-tf', $archivepath],
+ outfunc => sub {
+ my $line = shift;
+ $has_oci_layout = 1 if $line =~ /^oci-layout$/m;
+ $has_index_json = 1 if $line =~ /^index\.json$/m;
+ $has_blobs = 1 if $line =~ /^blobs\//m;
+ },
+ );
+
+ $is_oci = 1 if $has_oci_layout && $has_index_json && $has_blobs;
+ }
+
+ if ($is_oci) {
+ # Extract the OCI image
+ my ($id_map, undef, undef) = PVE::LXC::parse_id_maps($conf);
+ my $oci_config = PVE::LXC::Namespaces::run_in_userns(
+ sub {
+ PVE::RS::OCI::parse_and_extract_image(
+ $archivepath, $rootdir,
+ );
+ },
+ $id_map,
+ );
+
+ # Set the entrypoint and arguments if specified by the OCI image
+ my @init_cmd = ();
+ push(@init_cmd, @{ $oci_config->{Entrypoint} })
+ if $oci_config->{Entrypoint};
+ push(@init_cmd, @{ $oci_config->{Cmd} }) if $oci_config->{Cmd};
+ if (@init_cmd) {
+ my $init_cmd_str = shift(@init_cmd);
+ if (@init_cmd) {
+ $init_cmd_str .= ' ';
+ $init_cmd_str .= join(
+ ' ',
+ map {
+ my $s = $_;
+ $s =~ s/"/\\"/g;
+ qq{"$_"}
+ } @init_cmd,
+ );
+ }
+ if ($init_cmd_str ne '/sbin/init') {
+ push @{ $conf->{lxc} }, ['lxc.init.cmd', $init_cmd_str];
+
+ # An entrypoint other than /sbin/init breaks the tty console mode.
+ # This is fixed by setting cmode: console
+ $conf->{cmode} = 'console';
+ }
+ }
+
+ push @{ $conf->{lxc} }, ['lxc.init.cwd', $oci_config->{WorkingDir}]
+ if ($oci_config->{WorkingDir});
+
+ if (my $envs = $oci_config->{Env}) {
+ for my $env (@{$envs}) {
+ push @{ $conf->{lxc} }, ['lxc.environment', $env];
+ }
+ }
+
+ my $stop_signal = $oci_config->{StopSignal} // "SIGTERM";
+ push @{ $conf->{lxc} }, ['lxc.signal.halt', $stop_signal];
+ } else {
+ # Not an OCI image, so restore it as an LXC image instead
+ PVE::LXC::Create::restore_archive(
+ $storage_cfg,
+ $archive,
+ $rootdir,
+ $conf,
+ $ignore_unpack_errors,
+ $bwlimit,
+ );
+ }
if ($restore) {
print "merging backed-up and given configuration..\n";
--
2.47.2
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 21+ messages in thread
* Re: [pve-devel] [PATCH container v3 04/13] add support for OCI images as container templates
2025-07-09 12:34 ` [pve-devel] [PATCH container v3 04/13] add support for OCI images as container templates Filip Schauer
@ 2025-07-10 10:31 ` Wolfgang Bumiller
0 siblings, 0 replies; 21+ messages in thread
From: Wolfgang Bumiller @ 2025-07-10 10:31 UTC (permalink / raw)
To: Filip Schauer; +Cc: pve-devel
On Wed, Jul 09, 2025 at 02:34:21PM +0200, Filip Schauer wrote:
> This aims to add basic support for the Open Container Initiative image
> format according to the specification. [0]
>
> [0] https://github.com/opencontainers/image-spec/blob/main/spec.md
>
> Signed-off-by: Filip Schauer <f.schauer@proxmox.com>
> ---
> This patch depends on changes made to proxmox-perl-rs in patch 03/13.
> Meaning that proxmox-perl-rs needs to be bumped and a dependency & build
> dependency to libpve-rs-perl needs to be added to debian/control.
>
> Changed since v2:
> * rebase onto newest master (5a8b3f962f16) and re-format with
> proxmox-perltidy
> * check whether archive is an OCI image before trying to parse it as one
>
> Changed since v1:
> * fix entrypoint command missing Cmd
> * set lxc.signal.halt according to StopSignal (Fixes container shutdown)
>
> src/PVE/API2/LXC.pm | 96 ++++++++++++++++++++++++++++++++++++++++-----
> 1 file changed, 86 insertions(+), 10 deletions(-)
>
> diff --git a/src/PVE/API2/LXC.pm b/src/PVE/API2/LXC.pm
> index 28f7fdd..45c5cef 100644
> --- a/src/PVE/API2/LXC.pm
> +++ b/src/PVE/API2/LXC.pm
> @@ -19,9 +19,11 @@ use PVE::Storage;
> use PVE::RESTHandler;
> use PVE::RPCEnvironment;
> use PVE::ReplicationConfig;
> +use PVE::RS::OCI;
> use PVE::LXC;
> use PVE::LXC::Create;
> use PVE::LXC::Migrate;
> +use PVE::LXC::Namespaces;
> use PVE::GuestHelpers;
> use PVE::VZDump::Plugin;
> use PVE::API2::LXC::Config;
> @@ -523,19 +525,93 @@ __PACKAGE__->register_method({
>
> eval {
> my $rootdir = PVE::LXC::mount_all($vmid, $storage_cfg, $conf, 1);
> + my $archivepath = PVE::Storage::abs_filesystem_path($storage_cfg, $archive);
^ This should probably not happen when $archive is '-'.
> $bwlimit = PVE::Storage::get_bandwidth_limit(
> 'restore', [keys %used_storages], $bwlimit,
> );
> - print "restoring '$archive' now..\n"
> - if $restore && $archive ne '-';
> - PVE::LXC::Create::restore_archive(
> - $storage_cfg,
> - $archive,
> - $rootdir,
> - $conf,
> - $ignore_unpack_errors,
> - $bwlimit,
> - );
> + my $is_oci = 0;
> +
> + if ($restore && $archive ne '-') {
> + print "restoring '$archive' now..\n";
> + } elsif ($archivepath =~ /\.tar$/) {
> + # Check whether archive is an OCI image
> + my $has_oci_layout = 0;
> + my $has_index_json = 0;
> + my $has_blobs = 0;
> + PVE::Tools::run_command(
> + ['tar', '-tf', $archivepath],
> + outfunc => sub {
> + my $line = shift;
> + $has_oci_layout = 1 if $line =~ /^oci-layout$/m;
> + $has_index_json = 1 if $line =~ /^index\.json$/m;
^ The above 2 comparisons can just use `eq` instead of regexes.
> + $has_blobs = 1 if $line =~ /^blobs\//m;
> + },
> + );
> +
> + $is_oci = 1 if $has_oci_layout && $has_index_json && $has_blobs;
> + }
> +
> + if ($is_oci) {
> + # Extract the OCI image
> + my ($id_map, undef, undef) = PVE::LXC::parse_id_maps($conf);
> + my $oci_config = PVE::LXC::Namespaces::run_in_userns(
> + sub {
> + PVE::RS::OCI::parse_and_extract_image(
> + $archivepath, $rootdir,
> + );
> + },
> + $id_map,
> + );
> +
> + # Set the entrypoint and arguments if specified by the OCI image
> + my @init_cmd = ();
> + push(@init_cmd, @{ $oci_config->{Entrypoint} })
> + if $oci_config->{Entrypoint};
> + push(@init_cmd, @{ $oci_config->{Cmd} }) if $oci_config->{Cmd};
> + if (@init_cmd) {
> + my $init_cmd_str = shift(@init_cmd);
> + if (@init_cmd) {
> + $init_cmd_str .= ' ';
> + $init_cmd_str .= join(
> + ' ',
> + map {
> + my $s = $_;
> + $s =~ s/"/\\"/g;
> + qq{"$_"}
> + } @init_cmd,
> + );
> + }
> + if ($init_cmd_str ne '/sbin/init') {
> + push @{ $conf->{lxc} }, ['lxc.init.cmd', $init_cmd_str];
> +
> + # An entrypoint other than /sbin/init breaks the tty console mode.
> + # This is fixed by setting cmode: console
> + $conf->{cmode} = 'console';
> + }
> + }
> +
> + push @{ $conf->{lxc} }, ['lxc.init.cwd', $oci_config->{WorkingDir}]
> + if ($oci_config->{WorkingDir});
> +
> + if (my $envs = $oci_config->{Env}) {
> + for my $env (@{$envs}) {
> + push @{ $conf->{lxc} }, ['lxc.environment', $env];
^ As mentioned in the lxcfs patch - we cannot do this.
We could copy a small statically linked executable which reads
environment and init.cmd from a file, uses `setenv(3)` for each variable
before running `execv(3) (place them in the container as /.pve.init and
/.pve.env for example)
> + }
> + }
> +
> + my $stop_signal = $oci_config->{StopSignal} // "SIGTERM";
> + push @{ $conf->{lxc} }, ['lxc.signal.halt', $stop_signal];
> + } else {
> + # Not an OCI image, so restore it as an LXC image instead
> + PVE::LXC::Create::restore_archive(
> + $storage_cfg,
> + $archive,
> + $rootdir,
> + $conf,
> + $ignore_unpack_errors,
> + $bwlimit,
> + );
> + }
>
> if ($restore) {
> print "merging backed-up and given configuration..\n";
> --
> 2.47.2
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 21+ messages in thread
* [pve-devel] [PATCH container v3 05/13] config: add entrypoint parameter
2025-07-09 12:34 [pve-devel] [PATCH container/docs/lxcfs/manager/proxmox{, -perl-rs}/storage v3 00/13] support OCI images as container templates Filip Schauer
` (3 preceding siblings ...)
2025-07-09 12:34 ` [pve-devel] [PATCH container v3 04/13] add support for OCI images as container templates Filip Schauer
@ 2025-07-09 12:34 ` Filip Schauer
2025-07-09 12:34 ` [pve-devel] [PATCH container v3 06/13] configure static IP in LXC config for custom entrypoint Filip Schauer
` (7 subsequent siblings)
12 siblings, 0 replies; 21+ messages in thread
From: Filip Schauer @ 2025-07-09 12:34 UTC (permalink / raw)
To: pve-devel
Signed-off-by: Filip Schauer <f.schauer@proxmox.com>
---
Changed since v2:
* rebase onto newest master (5a8b3f962f16) and re-format with
proxmox-perltidy
src/PVE/API2/LXC.pm | 2 +-
src/PVE/LXC.pm | 2 ++
src/PVE/LXC/Config.pm | 12 ++++++++++++
3 files changed, 15 insertions(+), 1 deletion(-)
diff --git a/src/PVE/API2/LXC.pm b/src/PVE/API2/LXC.pm
index 45c5cef..c3d0037 100644
--- a/src/PVE/API2/LXC.pm
+++ b/src/PVE/API2/LXC.pm
@@ -582,7 +582,7 @@ __PACKAGE__->register_method({
);
}
if ($init_cmd_str ne '/sbin/init') {
- push @{ $conf->{lxc} }, ['lxc.init.cmd', $init_cmd_str];
+ $conf->{entrypoint} = $init_cmd_str;
# An entrypoint other than /sbin/init breaks the tty console mode.
# This is fixed by setting cmode: console
diff --git a/src/PVE/LXC.pm b/src/PVE/LXC.pm
index ffedcb9..37ff8e1 100644
--- a/src/PVE/LXC.pm
+++ b/src/PVE/LXC.pm
@@ -811,6 +811,8 @@ sub update_lxc_config {
$raw .= "lxc.rootfs.path = $dir/rootfs\n";
+ $raw .= "lxc.init.cmd = $conf->{entrypoint}\n" if defined($conf->{entrypoint});
+
foreach my $k (sort keys %$conf) {
next if $k !~ m/^net(\d+)$/;
my $ind = $1;
diff --git a/src/PVE/LXC/Config.pm b/src/PVE/LXC/Config.pm
index 7bdb8b9..fb370cf 100644
--- a/src/PVE/LXC/Config.pm
+++ b/src/PVE/LXC/Config.pm
@@ -638,6 +638,12 @@ my $confdesc = {
enum => ['shell', 'console', 'tty'],
default => 'tty',
},
+ entrypoint => {
+ optional => 1,
+ type => 'string',
+ description => "Absolute path from container rootfs to the binary to use as init.",
+ default => '/sbin/init',
+ },
protection => {
optional => 1,
type => 'boolean',
@@ -1844,6 +1850,12 @@ sub get_cmode {
return $conf->{cmode} // $confdesc->{cmode}->{default};
}
+sub get_entrypoint {
+ my ($class, $conf) = @_;
+
+ return $conf->{entrypoint} // $confdesc->{entrypoint}->{default};
+}
+
sub valid_volume_keys {
my ($class, $reverse) = @_;
--
2.47.2
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 21+ messages in thread
* [pve-devel] [PATCH container v3 06/13] configure static IP in LXC config for custom entrypoint
2025-07-09 12:34 [pve-devel] [PATCH container/docs/lxcfs/manager/proxmox{, -perl-rs}/storage v3 00/13] support OCI images as container templates Filip Schauer
` (4 preceding siblings ...)
2025-07-09 12:34 ` [pve-devel] [PATCH container v3 05/13] config: add entrypoint parameter Filip Schauer
@ 2025-07-09 12:34 ` Filip Schauer
2025-07-09 12:34 ` [pve-devel] [PATCH container v3 07/13] setup: debian: create /etc/network path if missing Filip Schauer
` (6 subsequent siblings)
12 siblings, 0 replies; 21+ messages in thread
From: Filip Schauer @ 2025-07-09 12:34 UTC (permalink / raw)
To: pve-devel
When a container uses the default `/sbin/init` entrypoint, network
interface configuration is usually managed by processes within the
container. However, containers with a different entrypoint might not
have any internal network management process. Consequently, IP addresses
might not be assigned.
This change ensures that a static IP address is explicitly set in the
LXC config for the container.
Signed-off-by: Filip Schauer <f.schauer@proxmox.com>
---
Changed since v2:
* rebase onto newest master (5a8b3f962f16) and re-format with
proxmox-perltidy
* add an "ipmanagehost" property to pct.conf to control whether network
interface IP configuration should be handled by the host.
src/PVE/API2/LXC.pm | 4 ++++
src/PVE/LXC.pm | 15 +++++++++++++++
src/PVE/LXC/Config.pm | 14 ++++++++++++++
3 files changed, 33 insertions(+)
diff --git a/src/PVE/API2/LXC.pm b/src/PVE/API2/LXC.pm
index c3d0037..096dfb8 100644
--- a/src/PVE/API2/LXC.pm
+++ b/src/PVE/API2/LXC.pm
@@ -587,6 +587,10 @@ __PACKAGE__->register_method({
# An entrypoint other than /sbin/init breaks the tty console mode.
# This is fixed by setting cmode: console
$conf->{cmode} = 'console';
+
+ # Manage the IP configuration for the container. A container with a
+ # custom entrypoint likely lacks internal network management.
+ $conf->{ipmanagehost} = 1;
}
}
diff --git a/src/PVE/LXC.pm b/src/PVE/LXC.pm
index 37ff8e1..7c9caf6 100644
--- a/src/PVE/LXC.pm
+++ b/src/PVE/LXC.pm
@@ -835,6 +835,21 @@ sub update_lxc_config {
if ($lxc_major >= 4) {
$raw .= "lxc.net.$ind.script.up = /usr/share/lxc/lxcnetaddbr\n";
}
+
+ if ((!defined($d->{link_down}) || $d->{link_down} != 1) && $conf->{ipmanagehost}) {
+ if (defined($d->{ip})) {
+ die "$k: DHCP is not supported with a custom entrypoint\n" if $d->{ip} eq 'dhcp';
+ $raw .= "lxc.net.$ind.ipv4.address = $d->{ip}\n" if $d->{ip} ne 'manual';
+ }
+ $raw .= "lxc.net.$ind.ipv4.gateway = $d->{gw}\n" if defined($d->{gw});
+ if (defined($d->{ip6})) {
+ die "$k: DHCPv6 and SLAAC are not supported with a custom entrypoint\n"
+ if $d->{ip6} =~ /^(auto|dhcp)$/;
+ $raw .= "lxc.net.$ind.ipv6.address = $d->{ip6}\n" if $d->{ip6} ne 'manual';
+ }
+ $raw .= "lxc.net.$ind.ipv6.gateway = $d->{gw6}\n" if defined($d->{gw6});
+ $raw .= "lxc.net.$ind.flags = up\n";
+ }
}
my $had_cpuset = 0;
diff --git a/src/PVE/LXC/Config.pm b/src/PVE/LXC/Config.pm
index fb370cf..c2f56e4 100644
--- a/src/PVE/LXC/Config.pm
+++ b/src/PVE/LXC/Config.pm
@@ -594,6 +594,12 @@ my $confdesc = {
. " This is saved as comment inside the configuration file.",
maxLength => 1024 * 8,
},
+ ipmanagehost => {
+ type => 'boolean',
+ description =>
+ "Whether this interface's IP configuration should be managed by the host.",
+ optional => 1,
+ },
searchdomain => {
optional => 1,
type => 'string',
@@ -1284,6 +1290,14 @@ sub update_pct_config {
die "$opt: MTU size '$mtu' is bigger than bridge MTU '$bridge_mtu'\n"
if ($mtu > $bridge_mtu);
}
+
+ if ((!defined($res->{link_down}) || $res->{link_down} != 1) && $conf->{ipmanagehost}) {
+ die "$opt: DHCP is not supported with a custom entrypoint\n"
+ if defined($res->{ip}) && $res->{ip} eq 'dhcp';
+
+ die "$opt: DHCPv6 and SLAAC are not supported with a custom entrypoint\n"
+ if defined($res->{ip6}) && $res->{ip6} =~ /^(auto|dhcp)$/;
+ }
} elsif ($opt =~ m/^dev(\d+)$/) {
my $device = $class->parse_device($value);
--
2.47.2
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 21+ messages in thread
* [pve-devel] [PATCH container v3 07/13] setup: debian: create /etc/network path if missing
2025-07-09 12:34 [pve-devel] [PATCH container/docs/lxcfs/manager/proxmox{, -perl-rs}/storage v3 00/13] support OCI images as container templates Filip Schauer
` (5 preceding siblings ...)
2025-07-09 12:34 ` [pve-devel] [PATCH container v3 06/13] configure static IP in LXC config for custom entrypoint Filip Schauer
@ 2025-07-09 12:34 ` Filip Schauer
2025-07-09 12:34 ` [pve-devel] [PATCH container v3 08/13] setup: recursively mkdir /etc/systemd/{network, system-preset} Filip Schauer
` (5 subsequent siblings)
12 siblings, 0 replies; 21+ messages in thread
From: Filip Schauer @ 2025-07-09 12:34 UTC (permalink / raw)
To: pve-devel
This prevents an error during Debian container setup when the
/etc/network directory is missing. This fixes container creation from
Debian based OCI images.
Signed-off-by: Filip Schauer <f.schauer@proxmox.com>
---
src/PVE/LXC/Setup/Debian.pm | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/PVE/LXC/Setup/Debian.pm b/src/PVE/LXC/Setup/Debian.pm
index c5926a9..55658ed 100644
--- a/src/PVE/LXC/Setup/Debian.pm
+++ b/src/PVE/LXC/Setup/Debian.pm
@@ -430,6 +430,7 @@ sub setup_network {
"auto lo\niface lo inet loopback\n" . "iface lo inet6 loopback\n\n" . $interfaces;
}
+ $self->ct_make_path('/etc/network');
$self->ct_file_set_contents($filename, $interfaces);
}
--
2.47.2
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 21+ messages in thread
* [pve-devel] [PATCH container v3 08/13] setup: recursively mkdir /etc/systemd/{network, system-preset}
2025-07-09 12:34 [pve-devel] [PATCH container/docs/lxcfs/manager/proxmox{, -perl-rs}/storage v3 00/13] support OCI images as container templates Filip Schauer
` (6 preceding siblings ...)
2025-07-09 12:34 ` [pve-devel] [PATCH container v3 07/13] setup: debian: create /etc/network path if missing Filip Schauer
@ 2025-07-09 12:34 ` Filip Schauer
2025-07-09 12:34 ` [pve-devel] [PATCH container v3 09/13] manage DHCP for containers with custom entrypoint Filip Schauer
` (4 subsequent siblings)
12 siblings, 0 replies; 21+ messages in thread
From: Filip Schauer @ 2025-07-09 12:34 UTC (permalink / raw)
To: pve-devel
Ensure that both /etc/systemd/network and /etc/systemd/system-preset
exist before writing files into them. This fixes container creation from
the docker.io/fedora & docker.io/ubuntu OCI images.
Signed-off-by: Filip Schauer <f.schauer@proxmox.com>
---
Changed since v2:
* rebase onto newest master (5a8b3f962f16) and re-format with
proxmox-perltidy
Introduced in v2
src/PVE/LXC/Setup/Base.pm | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/PVE/LXC/Setup/Base.pm b/src/PVE/LXC/Setup/Base.pm
index 6bdfb8d..892bb50 100644
--- a/src/PVE/LXC/Setup/Base.pm
+++ b/src/PVE/LXC/Setup/Base.pm
@@ -313,6 +313,7 @@ DATA
$data .= "IPv6AcceptRA = $accept_ra\n";
$data .= $routes if $routes;
+ $self->ct_make_path('/etc/systemd/network');
$self->ct_file_set_contents($filename, $data);
}
}
@@ -352,7 +353,7 @@ sub setup_systemd_preset {
}
}
- $self->ct_mkdir('/etc/systemd/system-preset', 0755);
+ $self->ct_make_path('/etc/systemd/system-preset');
$self->ct_file_set_contents(
'/etc/systemd/system-preset/00-pve.preset', $preset_data,
);
--
2.47.2
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 21+ messages in thread
* [pve-devel] [PATCH container v3 09/13] manage DHCP for containers with custom entrypoint
2025-07-09 12:34 [pve-devel] [PATCH container/docs/lxcfs/manager/proxmox{, -perl-rs}/storage v3 00/13] support OCI images as container templates Filip Schauer
` (7 preceding siblings ...)
2025-07-09 12:34 ` [pve-devel] [PATCH container v3 08/13] setup: recursively mkdir /etc/systemd/{network, system-preset} Filip Schauer
@ 2025-07-09 12:34 ` Filip Schauer
2025-07-09 13:41 ` Filip Schauer
2025-07-10 10:34 ` Wolfgang Bumiller
2025-07-09 12:34 ` [pve-devel] [PATCH lxcfs v3 10/13] lxc.mount.hook: override env variables from container config Filip Schauer
` (3 subsequent siblings)
12 siblings, 2 replies; 21+ messages in thread
From: Filip Schauer @ 2025-07-09 12:34 UTC (permalink / raw)
To: pve-devel
Containers that do not use the default `/sbin/init` entrypoint may lack
in-container network management. A previous commit already handles
static IP addresses. Now this commit also handles DHCP. This is done
using a `dhclient` process for each network interface.
Signed-off-by: Filip Schauer <f.schauer@proxmox.com>
---
Changed since v2:
* rebase onto newest master (5a8b3f962f16) and re-format with
proxmox-perltidy
* add an "ipmanagehost" property to pct.conf to control whether network
interface IP configuration should be handled by the host.
* manage_dhclient: add a FIXME comment regarding the AppArmor profile:
"use a profile that confines writes to /var/lib/lxc/$vmid and rootfs"
* kill_dhclients: untaint pid from pidfile
* fix manage_dhclient called with 'stop' instead of 'start' for IPv6
when container is started
src/PVE/LXC.pm | 103 +++++++++++++++++++++++++++++++++++++-----
src/PVE/LXC/Config.pm | 17 ++++---
2 files changed, 101 insertions(+), 19 deletions(-)
diff --git a/src/PVE/LXC.pm b/src/PVE/LXC.pm
index 7c9caf6..4f19a21 100644
--- a/src/PVE/LXC.pm
+++ b/src/PVE/LXC.pm
@@ -837,15 +837,12 @@ sub update_lxc_config {
}
if ((!defined($d->{link_down}) || $d->{link_down} != 1) && $conf->{ipmanagehost}) {
- if (defined($d->{ip})) {
- die "$k: DHCP is not supported with a custom entrypoint\n" if $d->{ip} eq 'dhcp';
- $raw .= "lxc.net.$ind.ipv4.address = $d->{ip}\n" if $d->{ip} ne 'manual';
- }
+ $raw .= "lxc.net.$ind.ipv4.address = $d->{ip}\n"
+ if defined($d->{ip}) && $d->{ip} !~ /^(dhcp|manual)$/;
$raw .= "lxc.net.$ind.ipv4.gateway = $d->{gw}\n" if defined($d->{gw});
if (defined($d->{ip6})) {
- die "$k: DHCPv6 and SLAAC are not supported with a custom entrypoint\n"
- if $d->{ip6} =~ /^(auto|dhcp)$/;
- $raw .= "lxc.net.$ind.ipv6.address = $d->{ip6}\n" if $d->{ip6} ne 'manual';
+ die "$k: SLAAC is not supported with a custom entrypoint\n" if $d->{ip6} eq 'auto';
+ $raw .= "lxc.net.$ind.ipv6.address = $d->{ip6}\n" if $d->{ip6} !~ /^(dhcp|manual)$/;
}
$raw .= "lxc.net.$ind.ipv6.gateway = $d->{gw6}\n" if defined($d->{gw6});
$raw .= "lxc.net.$ind.flags = up\n";
@@ -1028,6 +1025,8 @@ sub vm_stop_cleanup {
PVE::Storage::deactivate_volumes($storage_cfg, $vollist);
};
warn $@ if $@; # avoid errors - just warn
+
+ kill_dhclients($vmid, '*') if $conf->{ipmanagehost};
}
sub net_tap_plug : prototype($$) {
@@ -1259,6 +1258,52 @@ sub get_interfaces {
return $res;
}
+sub manage_dhclient {
+ my ($action, $vmid, $ipversion, $eth, $rootdir) = @_;
+
+ File::Path::make_path("/var/lib/lxc/$vmid/hook") if $action eq 'start';
+ my $pidfile = "/var/lib/lxc/$vmid/hook/dhclient$ipversion-$eth.pid";
+ my $leasefile = "/var/lib/lxc/$vmid/hook/dhclient$ipversion-$eth.leases";
+ my $scriptfile = '/usr/share/lxc/hooks/dhclient-script';
+ PVE::Tools::run_command([
+ 'lxc-attach',
+ '-n',
+ $vmid,
+ '-s',
+ 'NETWORK|UTSNAME',
+ '--',
+ 'aa-exec',
+ '-p',
+ 'unconfined', # FIXME: use a profile that confines writes to /var/lib/lxc/$vmid and rootfs
+ '/sbin/dhclient',
+ $action eq 'start' ? '-1' : '-r',
+ "-$ipversion",
+ '-pf',
+ $pidfile,
+ '-lf',
+ $leasefile,
+ '-e',
+ "ROOTFS=$rootdir",
+ '-sf',
+ $scriptfile,
+ $eth,
+ ]);
+}
+
+sub kill_dhclients {
+ my ($vmid, $eth) = @_;
+
+ foreach my $pidfile (glob("/var/lib/lxc/$vmid/hook/dhclient*-$eth.pid")) {
+ my $pid = eval { file_get_contents($pidfile) };
+ if (!$@) {
+ chomp $pid;
+ next if $pid !~ /^(\d+)$/;
+ kill 9, $1;
+ unlink($pidfile);
+ }
+ }
+}
+
sub update_ipconfig {
my ($vmid, $conf, $opt, $eth, $newnet, $rootdir) = @_;
@@ -1295,11 +1340,21 @@ sub update_ipconfig {
# step 1: add new IP, if this fails we cancel
my $is_real_ip = ($newip && $newip !~ /^(?:auto|dhcp|manual)$/);
- if ($change_ip && $is_real_ip) {
- eval { &$ipcmd($family_opt, 'addr', 'add', $newip, 'dev', $eth); };
- if (my $err = $@) {
- warn $err;
- return;
+ if ($change_ip) {
+ if ($conf->{ipmanagehost}) {
+ if ($newip && $newip eq 'dhcp') {
+ manage_dhclient('start', $vmid, $ipversion, $eth, $rootdir);
+ } elsif ($oldip && $oldip eq 'dhcp') {
+ manage_dhclient('stop', $vmid, $ipversion, $eth, $rootdir);
+ }
+ }
+
+ if ($is_real_ip) {
+ eval { &$ipcmd($family_opt, 'addr', 'add', $newip, 'dev', $eth); };
+ if (my $err = $@) {
+ warn $err;
+ return;
+ }
}
}
@@ -2827,6 +2882,30 @@ sub vm_start {
}
PVE::GuestHelpers::exec_hookscript($conf, $vmid, 'post-start');
+ if ($conf->{ipmanagehost}) {
+ my @dhcpv4_interfaces = ();
+ my @dhcpv6_interfaces = ();
+ foreach my $k (sort keys %$conf) {
+ next if $k !~ m/^net(\d+)$/;
+ my $d = PVE::LXC::Config->parse_lxc_network($conf->{$k});
+ push @dhcpv4_interfaces, $d->{name} if $d->{ip} && $d->{ip} eq 'dhcp';
+ push @dhcpv6_interfaces, $d->{name} if $d->{ip6} && $d->{ip6} eq 'dhcp';
+ }
+
+ my $pid = PVE::LXC::find_lxc_pid($vmid);
+ my $rootdir = "/proc/$pid/root";
+
+ foreach my $eth (@dhcpv4_interfaces) {
+ eval { manage_dhclient('start', $vmid, 4, $eth, $rootdir) };
+ PVE::RESTEnvironment::log_warn("DHCP failed - $@") if $@;
+ }
+
+ foreach my $eth (@dhcpv6_interfaces) {
+ eval { manage_dhclient('start', $vmid, 6, $eth, $rootdir) };
+ PVE::RESTEnvironment::log_warn("DHCP failed - $@") if $@;
+ }
+ }
+
return;
}
diff --git a/src/PVE/LXC/Config.pm b/src/PVE/LXC/Config.pm
index c2f56e4..39c3243 100644
--- a/src/PVE/LXC/Config.pm
+++ b/src/PVE/LXC/Config.pm
@@ -1291,12 +1291,13 @@ sub update_pct_config {
if ($mtu > $bridge_mtu);
}
- if ((!defined($res->{link_down}) || $res->{link_down} != 1) && $conf->{ipmanagehost}) {
- die "$opt: DHCP is not supported with a custom entrypoint\n"
- if defined($res->{ip}) && $res->{ip} eq 'dhcp';
-
- die "$opt: DHCPv6 and SLAAC are not supported with a custom entrypoint\n"
- if defined($res->{ip6}) && $res->{ip6} =~ /^(auto|dhcp)$/;
+ if (
+ (!defined($res->{link_down}) || $res->{link_down} != 1)
+ && $conf->{ipmanagehost}
+ && defined($res->{ip6})
+ && $res->{ip6} eq 'auto'
+ ) {
+ die "$opt: SLAAC is not supported with a custom entrypoint\n";
}
} elsif ($opt =~ m/^dev(\d+)$/) {
my $device = $class->parse_device($value);
@@ -1575,9 +1576,11 @@ sub vmconfig_hotplug_pending {
$cgroup->change_cpu_shares(undef);
} elsif ($opt =~ m/^net(\d)$/) {
my $netid = $1;
+ my $net = parse_lxc_network($conf->{$opt});
+ PVE::LXC::kill_dhclients($vmid, $net->{name}) if $conf->{ipmanagehost};
+
PVE::Network::veth_delete("veth${vmid}i$netid");
if ($have_sdn) {
- my $net = PVE::LXC::Config->parse_lxc_network($conf->{$opt});
print "delete ips from $opt\n";
eval {
PVE::Network::SDN::Vnets::del_ips_from_mac(
--
2.47.2
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 21+ messages in thread
* Re: [pve-devel] [PATCH container v3 09/13] manage DHCP for containers with custom entrypoint
2025-07-09 12:34 ` [pve-devel] [PATCH container v3 09/13] manage DHCP for containers with custom entrypoint Filip Schauer
@ 2025-07-09 13:41 ` Filip Schauer
2025-07-10 10:34 ` Wolfgang Bumiller
1 sibling, 0 replies; 21+ messages in thread
From: Filip Schauer @ 2025-07-09 13:41 UTC (permalink / raw)
To: pve-devel
On 09/07/2025 14:34, Filip Schauer wrote:
> @@ -2827,6 +2882,30 @@ sub vm_start { }
> PVE::GuestHelpers::exec_hookscript($conf, $vmid, 'post-start'); + if
> ($conf->{ipmanagehost}) { + my @dhcpv4_interfaces = (); + my
> @dhcpv6_interfaces = (); + foreach my $k (sort keys %$conf) { + next
> if $k !~ m/^net(\d+)$/; + my $d =
> PVE::LXC::Config->parse_lxc_network($conf->{$k}); + push
> @dhcpv4_interfaces, $d->{name} if $d->{ip} && $d->{ip} eq 'dhcp'; +
> push @dhcpv6_interfaces, $d->{name} if $d->{ip6} && $d->{ip6} eq
> 'dhcp'; + } + + my $pid = PVE::LXC::find_lxc_pid($vmid);
Replace this line with:
+ my ($pid, $pidfd) = PVE::LXC::open_lxc_pid($vmid);
to keep the $pidfd around for as long as $rootdir is in use.
> + my $rootdir = "/proc/$pid/root"; + + foreach my $eth
> (@dhcpv4_interfaces) { + eval { manage_dhclient('start', $vmid, 4,
> $eth, $rootdir) }; + PVE::RESTEnvironment::log_warn("DHCP failed -
> $@") if $@; + } + + foreach my $eth (@dhcpv6_interfaces) { + eval {
> manage_dhclient('start', $vmid, 6, $eth, $rootdir) }; +
> PVE::RESTEnvironment::log_warn("DHCP failed - $@") if $@; + } + } +
> return; }
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 21+ messages in thread
* Re: [pve-devel] [PATCH container v3 09/13] manage DHCP for containers with custom entrypoint
2025-07-09 12:34 ` [pve-devel] [PATCH container v3 09/13] manage DHCP for containers with custom entrypoint Filip Schauer
2025-07-09 13:41 ` Filip Schauer
@ 2025-07-10 10:34 ` Wolfgang Bumiller
1 sibling, 0 replies; 21+ messages in thread
From: Wolfgang Bumiller @ 2025-07-10 10:34 UTC (permalink / raw)
To: Filip Schauer; +Cc: pve-devel
On Wed, Jul 09, 2025 at 02:34:26PM +0200, Filip Schauer wrote:
> Containers that do not use the default `/sbin/init` entrypoint may lack
^ This commit message needs updating to reflect the changes in this
version.
> in-container network management. A previous commit already handles
> static IP addresses. Now this commit also handles DHCP. This is done
> using a `dhclient` process for each network interface.
>
> Signed-off-by: Filip Schauer <f.schauer@proxmox.com>
> ---
> Changed since v2:
> * rebase onto newest master (5a8b3f962f16) and re-format with
> proxmox-perltidy
> * add an "ipmanagehost" property to pct.conf to control whether network
> interface IP configuration should be handled by the host.
> * manage_dhclient: add a FIXME comment regarding the AppArmor profile:
> "use a profile that confines writes to /var/lib/lxc/$vmid and rootfs"
> * kill_dhclients: untaint pid from pidfile
> * fix manage_dhclient called with 'stop' instead of 'start' for IPv6
> when container is started
>
> src/PVE/LXC.pm | 103 +++++++++++++++++++++++++++++++++++++-----
> src/PVE/LXC/Config.pm | 17 ++++---
> 2 files changed, 101 insertions(+), 19 deletions(-)
>
> diff --git a/src/PVE/LXC.pm b/src/PVE/LXC.pm
> index 7c9caf6..4f19a21 100644
> --- a/src/PVE/LXC.pm
> +++ b/src/PVE/LXC.pm
> @@ -837,15 +837,12 @@ sub update_lxc_config {
> }
>
> if ((!defined($d->{link_down}) || $d->{link_down} != 1) && $conf->{ipmanagehost}) {
> - if (defined($d->{ip})) {
> - die "$k: DHCP is not supported with a custom entrypoint\n" if $d->{ip} eq 'dhcp';
> - $raw .= "lxc.net.$ind.ipv4.address = $d->{ip}\n" if $d->{ip} ne 'manual';
> - }
> + $raw .= "lxc.net.$ind.ipv4.address = $d->{ip}\n"
> + if defined($d->{ip}) && $d->{ip} !~ /^(dhcp|manual)$/;
> $raw .= "lxc.net.$ind.ipv4.gateway = $d->{gw}\n" if defined($d->{gw});
> if (defined($d->{ip6})) {
> - die "$k: DHCPv6 and SLAAC are not supported with a custom entrypoint\n"
> - if $d->{ip6} =~ /^(auto|dhcp)$/;
> - $raw .= "lxc.net.$ind.ipv6.address = $d->{ip6}\n" if $d->{ip6} ne 'manual';
> + die "$k: SLAAC is not supported with a custom entrypoint\n" if $d->{ip6} eq 'auto';
> + $raw .= "lxc.net.$ind.ipv6.address = $d->{ip6}\n" if $d->{ip6} !~ /^(dhcp|manual)$/;
> }
> $raw .= "lxc.net.$ind.ipv6.gateway = $d->{gw6}\n" if defined($d->{gw6});
> $raw .= "lxc.net.$ind.flags = up\n";
> @@ -1028,6 +1025,8 @@ sub vm_stop_cleanup {
> PVE::Storage::deactivate_volumes($storage_cfg, $vollist);
> };
> warn $@ if $@; # avoid errors - just warn
> +
> + kill_dhclients($vmid, '*') if $conf->{ipmanagehost};
> }
>
> sub net_tap_plug : prototype($$) {
> @@ -1259,6 +1258,52 @@ sub get_interfaces {
> return $res;
> }
>
> +sub manage_dhclient {
> + my ($action, $vmid, $ipversion, $eth, $rootdir) = @_;
> +
> + File::Path::make_path("/var/lib/lxc/$vmid/hook") if $action eq 'start';
> + my $pidfile = "/var/lib/lxc/$vmid/hook/dhclient$ipversion-$eth.pid";
> + my $leasefile = "/var/lib/lxc/$vmid/hook/dhclient$ipversion-$eth.leases";
> + my $scriptfile = '/usr/share/lxc/hooks/dhclient-script';
> + PVE::Tools::run_command([
> + 'lxc-attach',
> + '-n',
> + $vmid,
> + '-s',
> + 'NETWORK|UTSNAME',
> + '--',
> + 'aa-exec',
> + '-p',
> + 'unconfined', # FIXME: use a profile that confines writes to /var/lib/lxc/$vmid and rootfs
> + '/sbin/dhclient',
> + $action eq 'start' ? '-1' : '-r',
> + "-$ipversion",
> + '-pf',
> + $pidfile,
> + '-lf',
> + $leasefile,
> + '-e',
> + "ROOTFS=$rootdir",
> + '-sf',
> + $scriptfile,
> + $eth,
> + ]);
> +}
> +
> +sub kill_dhclients {
> + my ($vmid, $eth) = @_;
> +
> + foreach my $pidfile (glob("/var/lib/lxc/$vmid/hook/dhclient*-$eth.pid")) {
> + my $pid = eval { file_get_contents($pidfile) };
> + if (!$@) {
> + chomp $pid;
> + next if $pid !~ /^(\d+)$/;
> + kill 9, $1;
> + unlink($pidfile);
> + }
> + }
> +}
> +
> sub update_ipconfig {
> my ($vmid, $conf, $opt, $eth, $newnet, $rootdir) = @_;
>
> @@ -1295,11 +1340,21 @@ sub update_ipconfig {
>
> # step 1: add new IP, if this fails we cancel
> my $is_real_ip = ($newip && $newip !~ /^(?:auto|dhcp|manual)$/);
> - if ($change_ip && $is_real_ip) {
> - eval { &$ipcmd($family_opt, 'addr', 'add', $newip, 'dev', $eth); };
> - if (my $err = $@) {
> - warn $err;
> - return;
> + if ($change_ip) {
> + if ($conf->{ipmanagehost}) {
> + if ($newip && $newip eq 'dhcp') {
> + manage_dhclient('start', $vmid, $ipversion, $eth, $rootdir);
> + } elsif ($oldip && $oldip eq 'dhcp') {
> + manage_dhclient('stop', $vmid, $ipversion, $eth, $rootdir);
> + }
> + }
> +
> + if ($is_real_ip) {
> + eval { &$ipcmd($family_opt, 'addr', 'add', $newip, 'dev', $eth); };
> + if (my $err = $@) {
> + warn $err;
> + return;
> + }
> }
> }
>
> @@ -2827,6 +2882,30 @@ sub vm_start {
> }
> PVE::GuestHelpers::exec_hookscript($conf, $vmid, 'post-start');
>
> + if ($conf->{ipmanagehost}) {
> + my @dhcpv4_interfaces = ();
> + my @dhcpv6_interfaces = ();
> + foreach my $k (sort keys %$conf) {
> + next if $k !~ m/^net(\d+)$/;
> + my $d = PVE::LXC::Config->parse_lxc_network($conf->{$k});
> + push @dhcpv4_interfaces, $d->{name} if $d->{ip} && $d->{ip} eq 'dhcp';
> + push @dhcpv6_interfaces, $d->{name} if $d->{ip6} && $d->{ip6} eq 'dhcp';
> + }
> +
> + my $pid = PVE::LXC::find_lxc_pid($vmid);
> + my $rootdir = "/proc/$pid/root";
> +
> + foreach my $eth (@dhcpv4_interfaces) {
> + eval { manage_dhclient('start', $vmid, 4, $eth, $rootdir) };
> + PVE::RESTEnvironment::log_warn("DHCP failed - $@") if $@;
> + }
> +
> + foreach my $eth (@dhcpv6_interfaces) {
> + eval { manage_dhclient('start', $vmid, 6, $eth, $rootdir) };
> + PVE::RESTEnvironment::log_warn("DHCP failed - $@") if $@;
> + }
> + }
> +
> return;
> }
>
> diff --git a/src/PVE/LXC/Config.pm b/src/PVE/LXC/Config.pm
> index c2f56e4..39c3243 100644
> --- a/src/PVE/LXC/Config.pm
> +++ b/src/PVE/LXC/Config.pm
> @@ -1291,12 +1291,13 @@ sub update_pct_config {
> if ($mtu > $bridge_mtu);
> }
>
> - if ((!defined($res->{link_down}) || $res->{link_down} != 1) && $conf->{ipmanagehost}) {
> - die "$opt: DHCP is not supported with a custom entrypoint\n"
> - if defined($res->{ip}) && $res->{ip} eq 'dhcp';
> -
> - die "$opt: DHCPv6 and SLAAC are not supported with a custom entrypoint\n"
> - if defined($res->{ip6}) && $res->{ip6} =~ /^(auto|dhcp)$/;
> + if (
> + (!defined($res->{link_down}) || $res->{link_down} != 1)
> + && $conf->{ipmanagehost}
> + && defined($res->{ip6})
> + && $res->{ip6} eq 'auto'
> + ) {
> + die "$opt: SLAAC is not supported with a custom entrypoint\n";
> }
> } elsif ($opt =~ m/^dev(\d+)$/) {
> my $device = $class->parse_device($value);
> @@ -1575,9 +1576,11 @@ sub vmconfig_hotplug_pending {
> $cgroup->change_cpu_shares(undef);
> } elsif ($opt =~ m/^net(\d)$/) {
> my $netid = $1;
> + my $net = parse_lxc_network($conf->{$opt});
> + PVE::LXC::kill_dhclients($vmid, $net->{name}) if $conf->{ipmanagehost};
> +
> PVE::Network::veth_delete("veth${vmid}i$netid");
> if ($have_sdn) {
> - my $net = PVE::LXC::Config->parse_lxc_network($conf->{$opt});
> print "delete ips from $opt\n";
> eval {
> PVE::Network::SDN::Vnets::del_ips_from_mac(
> --
> 2.47.2
>
>
>
> _______________________________________________
> pve-devel mailing list
> pve-devel@lists.proxmox.com
> https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
>
>
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 21+ messages in thread
* [pve-devel] [PATCH lxcfs v3 10/13] lxc.mount.hook: override env variables from container config
2025-07-09 12:34 [pve-devel] [PATCH container/docs/lxcfs/manager/proxmox{, -perl-rs}/storage v3 00/13] support OCI images as container templates Filip Schauer
` (8 preceding siblings ...)
2025-07-09 12:34 ` [pve-devel] [PATCH container v3 09/13] manage DHCP for containers with custom entrypoint Filip Schauer
@ 2025-07-09 12:34 ` Filip Schauer
2025-07-10 9:30 ` Wolfgang Bumiller
2025-07-09 12:34 ` [pve-devel] [PATCH storage v3 11/13] allow .tar container templates Filip Schauer
` (2 subsequent siblings)
12 siblings, 1 reply; 21+ messages in thread
From: Filip Schauer @ 2025-07-09 12:34 UTC (permalink / raw)
To: pve-devel
This can still break `/bin/sh` if an OCI image injects a different
`libc.so.6` with $LD_LIBRARY_PATH.
Signed-off-by: Filip Schauer <f.schauer@proxmox.com>
---
Arbitrary code execution is theoretically still possible with a
specially crafted OCI image that provides a shared library and points
$LD_LIBRARY_PATH to its parent directory. Although the code is confined
to the container's namespace, it can still see the host file system.
While this may not pose a significant security risk, it is nonetheless
suboptimal. I am unsure about the best way to fully mitigate this.
Introduced in v3
.../patches/reset-path-to-host-defaults.patch | 38 +++++++++++++++++++
debian/patches/series | 1 +
2 files changed, 39 insertions(+)
create mode 100644 debian/patches/reset-path-to-host-defaults.patch
diff --git a/debian/patches/reset-path-to-host-defaults.patch b/debian/patches/reset-path-to-host-defaults.patch
new file mode 100644
index 0000000..12f150d
--- /dev/null
+++ b/debian/patches/reset-path-to-host-defaults.patch
@@ -0,0 +1,38 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Filip Schauer <f.schauer@proxmox.com>
+Date: Mon, 23 Jun 2025 13:05:35 +0200
+Subject: [PATCH] lxc.mount.hook: override env variables from container
+ config
+
+Without this, if the container config specifies a custom PATH variable
+via lxc.environment that omits /usr/bin or /bin, binaries like
+`readlink` and `mount` may not be found, causing container startup to
+fail.
+
+Fixes startup breakage with images like `ghcr.io/nixos/nix:latest`.
+
+This also mitigates arbitrary code execution during container startup
+before pivot_root (albeit confined in its own namespace) with a
+specially crafted OCI image providing a custom `readlink` or `mount`
+binary and pointing the PATH variable to it.
+
+Signed-off-by: Filip Schauer <f.schauer@proxmox.com>
+---
+ share/lxc.mount.hook.in | 4 ++++
+ 1 file changed, 4 insertions(+)
+
+diff --git a/share/lxc.mount.hook.in b/share/lxc.mount.hook.in
+index 6fd13b0..a25a5ef 100755
+--- a/share/lxc.mount.hook.in
++++ b/share/lxc.mount.hook.in
+@@ -11,6 +11,10 @@ do
+ shift
+ done
+
++# Set the PATH variable in case it was modified by lxc.environment
++PATH=/usr/bin:/bin
++LD_LIBRARY_PATH=
++
+ # We're dealing with mount entries, so expand any symlink
+ LXC_ROOTFS_MOUNT=$(readlink -f "${LXC_ROOTFS_MOUNT}")
+
diff --git a/debian/patches/series b/debian/patches/series
index bf650b4..f3391c0 100644
--- a/debian/patches/series
+++ b/debian/patches/series
@@ -1 +1,2 @@
do-not-start-without-lxcfs.patch
+reset-path-to-host-defaults.patch
--
2.47.2
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 21+ messages in thread
* Re: [pve-devel] [PATCH lxcfs v3 10/13] lxc.mount.hook: override env variables from container config
2025-07-09 12:34 ` [pve-devel] [PATCH lxcfs v3 10/13] lxc.mount.hook: override env variables from container config Filip Schauer
@ 2025-07-10 9:30 ` Wolfgang Bumiller
0 siblings, 0 replies; 21+ messages in thread
From: Wolfgang Bumiller @ 2025-07-10 9:30 UTC (permalink / raw)
To: Filip Schauer; +Cc: pve-devel
NAK
This needs to be handled differently.
Before this series `lxc.environment` could not be set at all except by
manually modifying the config as *root*.
If we want to support the `Env` key in OCI images, we need to either
replace the `init` command with a wrapper setting that environment
before running the final command, or lxc itself needs to learn a new
configuration for this (eg. an `lxc.environment.runtime`).
On Wed, Jul 09, 2025 at 02:34:27PM +0200, Filip Schauer wrote:
> This can still break `/bin/sh` if an OCI image injects a different
> `libc.so.6` with $LD_LIBRARY_PATH.
>
> Signed-off-by: Filip Schauer <f.schauer@proxmox.com>
> ---
> Arbitrary code execution is theoretically still possible with a
> specially crafted OCI image that provides a shared library and points
> $LD_LIBRARY_PATH to its parent directory. Although the code is confined
> to the container's namespace, it can still see the host file system.
> While this may not pose a significant security risk, it is nonetheless
> suboptimal. I am unsure about the best way to fully mitigate this.
>
> Introduced in v3
>
> .../patches/reset-path-to-host-defaults.patch | 38 +++++++++++++++++++
> debian/patches/series | 1 +
> 2 files changed, 39 insertions(+)
> create mode 100644 debian/patches/reset-path-to-host-defaults.patch
>
> diff --git a/debian/patches/reset-path-to-host-defaults.patch b/debian/patches/reset-path-to-host-defaults.patch
> new file mode 100644
> index 0000000..12f150d
> --- /dev/null
> +++ b/debian/patches/reset-path-to-host-defaults.patch
> @@ -0,0 +1,38 @@
> +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
> +From: Filip Schauer <f.schauer@proxmox.com>
> +Date: Mon, 23 Jun 2025 13:05:35 +0200
> +Subject: [PATCH] lxc.mount.hook: override env variables from container
> + config
> +
> +Without this, if the container config specifies a custom PATH variable
> +via lxc.environment that omits /usr/bin or /bin, binaries like
> +`readlink` and `mount` may not be found, causing container startup to
> +fail.
> +
> +Fixes startup breakage with images like `ghcr.io/nixos/nix:latest`.
> +
> +This also mitigates arbitrary code execution during container startup
> +before pivot_root (albeit confined in its own namespace) with a
> +specially crafted OCI image providing a custom `readlink` or `mount`
> +binary and pointing the PATH variable to it.
> +
> +Signed-off-by: Filip Schauer <f.schauer@proxmox.com>
> +---
> + share/lxc.mount.hook.in | 4 ++++
> + 1 file changed, 4 insertions(+)
> +
> +diff --git a/share/lxc.mount.hook.in b/share/lxc.mount.hook.in
> +index 6fd13b0..a25a5ef 100755
> +--- a/share/lxc.mount.hook.in
> ++++ b/share/lxc.mount.hook.in
> +@@ -11,6 +11,10 @@ do
> + shift
> + done
> +
> ++# Set the PATH variable in case it was modified by lxc.environment
> ++PATH=/usr/bin:/bin
> ++LD_LIBRARY_PATH=
> ++
> + # We're dealing with mount entries, so expand any symlink
> + LXC_ROOTFS_MOUNT=$(readlink -f "${LXC_ROOTFS_MOUNT}")
> +
> diff --git a/debian/patches/series b/debian/patches/series
> index bf650b4..f3391c0 100644
> --- a/debian/patches/series
> +++ b/debian/patches/series
> @@ -1 +1,2 @@
> do-not-start-without-lxcfs.patch
> +reset-path-to-host-defaults.patch
> --
> 2.47.2
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 21+ messages in thread
* [pve-devel] [PATCH storage v3 11/13] allow .tar container templates
2025-07-09 12:34 [pve-devel] [PATCH container/docs/lxcfs/manager/proxmox{, -perl-rs}/storage v3 00/13] support OCI images as container templates Filip Schauer
` (9 preceding siblings ...)
2025-07-09 12:34 ` [pve-devel] [PATCH lxcfs v3 10/13] lxc.mount.hook: override env variables from container config Filip Schauer
@ 2025-07-09 12:34 ` Filip Schauer
2025-07-09 12:34 ` [pve-devel] [PATCH manager v3 12/13] ui: storage upload: accept *.tar files as vztmpl Filip Schauer
2025-07-09 12:34 ` [pve-devel] [PATCH docs v3 13/13] ct: add OCI image docs Filip Schauer
12 siblings, 0 replies; 21+ messages in thread
From: Filip Schauer @ 2025-07-09 12:34 UTC (permalink / raw)
To: pve-devel
This is needed for OCI container images bundled as tar files, as
generated by `docker save`. OCI images do not need additional
compression, since the content is usually compressed already.
Signed-off-by: Filip Schauer <f.schauer@proxmox.com>
---
Changed since v2:
* Modify VZTMPL_EXT_RE_1 regex to put "tar" into capture group when
matching on a .tar file.
src/PVE/Storage.pm | 2 +-
src/PVE/Storage/Plugin.pm | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/PVE/Storage.pm b/src/PVE/Storage.pm
index 7861bf6..5eca719 100755
--- a/src/PVE/Storage.pm
+++ b/src/PVE/Storage.pm
@@ -115,7 +115,7 @@ PVE::Storage::Plugin->init();
our $ISO_EXT_RE_0 = qr/\.(?:iso|img)/i;
-our $VZTMPL_EXT_RE_1 = qr/\.tar\.(gz|xz|zst|bz2)/i;
+our $VZTMPL_EXT_RE_1 = qr/\.(?|(tar)(?!\.)|tar\.(gz|xz|zst|bz2))/i;
our $BACKUP_EXT_RE_2 = qr/\.(tgz|(?:tar|vma)(?:\.(${\PVE::Storage::Plugin::COMPRESSOR_RE}))?)/;
diff --git a/src/PVE/Storage/Plugin.pm b/src/PVE/Storage/Plugin.pm
index c2f376b..1689c94 100644
--- a/src/PVE/Storage/Plugin.pm
+++ b/src/PVE/Storage/Plugin.pm
@@ -1429,7 +1429,7 @@ my $get_subdir_files = sub {
} elsif ($tt eq 'vztmpl') {
next if $fn !~ m!/([^/]+$PVE::Storage::VZTMPL_EXT_RE_1)$!;
- $info = { volid => "$sid:vztmpl/$1", format => "t$2" };
+ $info = { volid => "$sid:vztmpl/$1", format => $2 eq 'tar' ? $2 : "t$2" };
} elsif ($tt eq 'backup') {
next if $fn !~ m!/([^/]+$PVE::Storage::BACKUP_EXT_RE_2)$!;
--
2.47.2
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 21+ messages in thread
* [pve-devel] [PATCH manager v3 12/13] ui: storage upload: accept *.tar files as vztmpl
2025-07-09 12:34 [pve-devel] [PATCH container/docs/lxcfs/manager/proxmox{, -perl-rs}/storage v3 00/13] support OCI images as container templates Filip Schauer
` (10 preceding siblings ...)
2025-07-09 12:34 ` [pve-devel] [PATCH storage v3 11/13] allow .tar container templates Filip Schauer
@ 2025-07-09 12:34 ` Filip Schauer
2025-07-09 12:34 ` [pve-devel] [PATCH docs v3 13/13] ct: add OCI image docs Filip Schauer
12 siblings, 0 replies; 21+ messages in thread
From: Filip Schauer @ 2025-07-09 12:34 UTC (permalink / raw)
To: pve-devel
Signed-off-by: Filip Schauer <f.schauer@proxmox.com>
---
This depends on the change made to pve-storage in patch 11/13.
It might make sense to bump pve-storage and with it bump the dependency
to libpve-storage-perl in debian/control.
Changed since v2:
* rebase onto newest master (84b22751f211) and re-format
Introduced in v2
www/manager6/window/UploadToStorage.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/www/manager6/window/UploadToStorage.js b/www/manager6/window/UploadToStorage.js
index cc53596d..df6a9178 100644
--- a/www/manager6/window/UploadToStorage.js
+++ b/www/manager6/window/UploadToStorage.js
@@ -11,7 +11,7 @@ Ext.define('PVE.window.UploadToStorage', {
acceptedExtensions: {
import: ['.ova', '.qcow2', '.raw', '.vmdk'],
iso: ['.img', '.iso'],
- vztmpl: ['.tar.gz', '.tar.xz', '.tar.zst'],
+ vztmpl: ['.tar', '.tar.gz', '.tar.xz', '.tar.zst'],
},
// accepted for file selection, will be renamed to real extension
--
2.47.2
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 21+ messages in thread
* [pve-devel] [PATCH docs v3 13/13] ct: add OCI image docs
2025-07-09 12:34 [pve-devel] [PATCH container/docs/lxcfs/manager/proxmox{, -perl-rs}/storage v3 00/13] support OCI images as container templates Filip Schauer
` (11 preceding siblings ...)
2025-07-09 12:34 ` [pve-devel] [PATCH manager v3 12/13] ui: storage upload: accept *.tar files as vztmpl Filip Schauer
@ 2025-07-09 12:34 ` Filip Schauer
12 siblings, 0 replies; 21+ messages in thread
From: Filip Schauer @ 2025-07-09 12:34 UTC (permalink / raw)
To: pve-devel
Signed-off-by: Filip Schauer <f.schauer@proxmox.com>
---
Introduced in v3
pct.adoc | 72 +++++++++++++++++++++++++++++++++++++++++++++++++-------
1 file changed, 64 insertions(+), 8 deletions(-)
diff --git a/pct.adoc b/pct.adoc
index 529b72f..b538f56 100644
--- a/pct.adoc
+++ b/pct.adoc
@@ -54,15 +54,22 @@ the cluster setup, and they can use the same network and storage resources as
virtual machines. You can also use the {pve} firewall, or manage containers
using the HA framework.
-Our primary goal is to offer an environment that provides the benefits of using a
-VM, but without the additional overhead. This means that Proxmox Containers can
-be categorized as ``System Containers'', rather than ``Application Containers''.
+Our primary goal has traditionally been to offer an environment that provides
+the benefits of using a VM, but without the additional overhead. This means that
+Proxmox Containers have been primarily categorized as ``System Containers''.
-NOTE: If you want to run application containers, for example, 'Docker' images, it
-is recommended that you run them inside a Proxmox QEMU VM. This will give you
-all the advantages of application containerization, while also providing the
-benefits that VMs offer, such as strong isolation from the host and the ability
-to live-migrate, which otherwise isn't possible with containers.
+With the introduction of OCI (**O**pen **C**ontainer **I**nitiative) image support,
+Proxmox VE now also integrates ``Application Containers''. When creating a
+container from an OCI image, the image is automatically converted to the
+LXC stack that Proxmox VE uses.
+
+This approach allows users to benefit from a wide ecosystem of pre-packaged
+applications while retaining the robust management features of Proxmox VE.
+
+While running lightweight ``Application Containers'' directly offers significant
+advantages over a full VM, for use cases demanding maximum isolation and
+the ability to live-migrate, nesting containers inside a Proxmox QEMU VM remains
+a recommended practice.
Technology Overview
@@ -256,6 +263,12 @@ Container Images
Container images, sometimes also referred to as ``templates'' or
``appliances'', are `tar` archives which contain everything to run a container.
+Proxmox VE can utilize two main types of images: *System Container Templates*
+for creating full virtual environments, and *Application Container Images* based
+on the OCI standard for running specific applications.
+
+System Container Templates
+~~~~~~~~~~~~~~~~~~~~~~~~~~
{pve} itself provides a variety of basic templates for the
xref:pct_supported_distributions[most common Linux distributions]. They can be
@@ -336,6 +349,49 @@ delete that image later with:
# pveam remove local:vztmpl/debian-10.0-standard_10.0-1_amd64.tar.gz
----
+Open Container Initiative (OCI) Images (Experimental)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Proxmox VE can also use OCI images to create containers. This makes it easy to
+run pre-packaged applications. A container created from an OCI image still uses
+the existing LXC framework.
+
+Obtaining OCI Images
+~~~~~~~~~~~~~~~~~~~~
+
+An OCI image can be pulled from a registry and saved as a `tar` archive. You can
+use tools like `docker`, `podman`, or `skopeo` for this. For example, to obtain
+the Apache HTTP Server (httpd) image from Docker Hub:
+
+Using Docker:
+
+----
+# docker pull httpd
+# docker save httpd > httpd.tar
+----
+
+Using Podman:
+
+When using Podman, the archive format must be explicitly specified as
+`oci-archive`.
+
+----
+# podman pull httpd
+# podman save --format=oci-archive httpd > httpd.tar
+----
+
+Using skopeo:
+
+----
+# skopeo copy docker://httpd:latest oci-archive:httpd.tar:latest
+----
+
+The resulting `httpd.tar` archive is an OCI image. It can be uploaded to a
+Proxmox VE storage that supports container templates via the *Upload* button in
+the storage content view of the web interface.
+
+Once the template is on a storage, you can create the container with
+`pct create` or use the wizard in the web interface.
[[pct_settings]]
Container Settings
--
2.47.2
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 21+ messages in thread