* [pve-devel] [PATCH container/manager/proxmox{, -perl-rs}/storage v2 00/11] support OCI images as container templates
@ 2025-06-11 14:48 Filip Schauer
2025-06-11 14:48 ` [pve-devel] [PATCH proxmox v2 01/11] add proxmox-oci crate Filip Schauer
` (12 more replies)
0 siblings, 13 replies; 29+ messages in thread
From: Filip Schauer @ 2025-06-11 14:48 UTC (permalink / raw)
To: pve-devel
Add basic support for OCI (Open Container Initiative) images [0] as
container templates.
An OCI image can be for example obtained from Docker Hub:
Either using Docker:
```
$ docker pull httpd
$ docker save httpd > httpd.tar
```
Or using Podman:
When using Podman, the format needs to be explicitly specified,
otherwise it defaults to docker-archive.
```
$ podman pull httpd
$ podman save --format=oci-archive httpd > httpd.tar
```
The tarball can be uploaded to a storage as a container template and
then used during container creation. It is automatically detected that
the container template is an OCI image. The resulting container still
uses the existing LXC framework.
# Dependencies:
To be able to build `proxmox-oci`, the `oci-spec` crate is required as a
dependency. A patch from Christoph [1] packages the `oci-spec` crate as
a deb package. Alternatively if the `oci-spec` crate is not yet
packaged, it can be downloaded from crates.io.
Here is a little script to download the `oci-spec` crate along with its
dependencies:
```sh
download_crate() {
CRATE_NAME=$1
CRATE_VERSION=$2
CRATE_SHA256=$3
wget https://crates.io/api/v1/crates/$CRATE_NAME/$CRATE_VERSION/download
COMPUTED_SHA256=$(sha256sum download | awk '{ print $1 }')
if [ "$COMPUTED_SHA256" != "$CRATE_SHA256" ]; then
echo "Checksum mismatch"; exit 1
fi
tar -xf download
rm download
mv $CRATE_NAME-$CRATE_VERSION /usr/share/cargo/registry/
echo "{\"package\":\"$CRATE_SHA256\",\"files\":{}}" > /usr/share/cargo/registry/$CRATE_NAME-$CRATE_VERSION/.cargo-checksum.json
}
download_crate strsim 0.11.1 7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f
download_crate ident_case 1.0.1 b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39
download_crate darling_macro 0.20.11 fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead
download_crate darling_core 0.20.11 0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e
download_crate darling 0.20.11 fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee
download_crate proc-macro-error-attr2 2.0.0 96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5
download_crate derive_builder_core 0.20.2 2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8
download_crate thiserror-impl 2.0.0 22efd00f33f93fa62848a7cab956c3d38c8d43095efda1decfc2b3a5dc0b8972
download_crate rustversion 1.0.20 eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2
download_crate heck 0.5.0 2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea
download_crate proc-macro-error2 2.0.1 11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802
download_crate derive_builder_macro 0.20.2 ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c
download_crate thiserror 2.0.0 15291287e9bff1bc6f9ff3409ed9af665bec7a5fc8ac079ea96be07bca0e2668
download_crate strum_macros 0.27.1 c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8
download_crate strum 0.27.1 f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32
download_crate getset 0.1.5 f3586f256131df87204eb733da72e3d3eb4f343c639f4b7be279ac7c48baeafe
download_crate derive_builder 0.20.2 507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947
download_crate oci-spec 0.8.1 57e9beda9d92fac7bf4904c34c83340ef1024159faee67179a04e0277523da33
```
Since librust-oci-spec-dev is in the proxmox-oci/debian/control file, a
dummy package needs to be installed, so dpkg-checkbuilddeps does not
complain.
dummy_librust_oci_spec.equivs:
```
Package: librust-oci-spec-dev
Version: 0.8.1
Provides: librust-oci-spec-0.8+default-dev (= 0.8.1-1)
```
```
$ equivs-build dummy_librust_oci_spec.equivs
$ dpkg -i ./librust-oci-spec-dev_0.8.1_all.deb
```
# Build & install order:
OCI image support:
1. proxmox
2. proxmox-perl-rs
3. pve-container
.tar container template support:
1. pve-storage
2. pve-manager
[0] https://github.com/opencontainers/image-spec/blob/main/spec.md
[1] https://lore.proxmox.com/pve-devel/20250606103719.533030-2-c.heiss@proxmox.com/
Changed since v1:
* Fix entrypoint command missing Cmd
* Set lxc.signal.halt according to StopSignal (Fixes container shutdown)
* setup: Ensure that both /etc/systemd/network and
/etc/systemd/system-preset exist before writing files into them.
* ui: storage upload: accept *.tar files as vztmpl
* proxmox-perl-rs: rebase on latest master (3d9806cb3c7f)
* proxmox-perl-rs: add new dependencies to debian/control
* proxmox-oci: refactor errors and use `thiserror` to avoid boilerplate
proxmox:
Filip Schauer (1):
add proxmox-oci crate
Cargo.toml | 1 +
proxmox-oci/Cargo.toml | 22 ++++
proxmox-oci/debian/changelog | 5 +
proxmox-oci/debian/control | 47 ++++++++
proxmox-oci/debian/debcargo.toml | 7 ++
proxmox-oci/src/lib.rs | 196 +++++++++++++++++++++++++++++++
proxmox-oci/src/oci_tar_image.rs | 167 ++++++++++++++++++++++++++
7 files changed, 445 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
proxmox-perl-rs:
Filip Schauer (1):
add Perl mapping for OCI container image parser/extractor
pve-rs/Cargo.toml | 2 ++
pve-rs/Makefile | 1 +
pve-rs/debian/control | 2 ++
pve-rs/src/lib.rs | 1 +
pve-rs/src/oci.rs | 20 ++++++++++++++++++++
5 files changed, 26 insertions(+)
create mode 100644 pve-rs/src/oci.rs
pve-container:
Filip Schauer (7):
config: whitelist lxc.init.cwd
add support for OCI images as container templates
config: add entrypoint parameter
configure static IP in LXC config for custom entrypoint
setup: debian: create /etc/network path if missing
setup: recursively mkdir /etc/systemd/{network,system-preset}
manage DHCP for containers with custom entrypoint
src/PVE/API2/LXC.pm | 64 +++++++++++++++++++++++++--
src/PVE/LXC.pm | 88 ++++++++++++++++++++++++++++++++++---
src/PVE/LXC/Config.pm | 19 +++++++-
src/PVE/LXC/Setup/Base.pm | 3 +-
src/PVE/LXC/Setup/Debian.pm | 1 +
5 files changed, 165 insertions(+), 10 deletions(-)
pve-storage:
Filip Schauer (1):
allow .tar container templates
src/PVE/Storage.pm | 2 +-
src/PVE/Storage/Plugin.pm | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
pve-manager:
Filip Schauer (1):
ui: storage upload: accept *.tar files as vztmpl
www/manager6/window/UploadToStorage.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
Summary over all repositories:
20 files changed, 639 insertions(+), 13 deletions(-)
--
Generated by git-murpp 0.6.0
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 29+ messages in thread
* [pve-devel] [PATCH proxmox v2 01/11] add proxmox-oci crate
2025-06-11 14:48 [pve-devel] [PATCH container/manager/proxmox{, -perl-rs}/storage v2 00/11] support OCI images as container templates Filip Schauer
@ 2025-06-11 14:48 ` Filip Schauer
2025-06-24 12:42 ` Wolfgang Bumiller
2025-06-25 8:13 ` Wolfgang Bumiller
2025-06-11 14:48 ` [pve-devel] [PATCH proxmox-perl-rs v2 02/11] add Perl mapping for OCI container image parser/extractor Filip Schauer
` (11 subsequent siblings)
12 siblings, 2 replies; 29+ messages in thread
From: Filip Schauer @ 2025-06-11 14:48 UTC (permalink / raw)
To: pve-devel
This crate can parse and extract an OCI image bundled as a tar archive.
Signed-off-by: Filip Schauer <f.schauer@proxmox.com>
---
Cargo.toml | 1 +
proxmox-oci/Cargo.toml | 22 ++++
proxmox-oci/debian/changelog | 5 +
proxmox-oci/debian/control | 47 ++++++++
proxmox-oci/debian/debcargo.toml | 7 ++
proxmox-oci/src/lib.rs | 196 +++++++++++++++++++++++++++++++
proxmox-oci/src/oci_tar_image.rs | 167 ++++++++++++++++++++++++++
7 files changed, 445 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 bf9e83d7..8365b18a 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-rest-server",
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..3974cf48
--- /dev/null
+++ b/proxmox-oci/debian/control
@@ -0,0 +1,47 @@
+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.1.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.12+bindgen-dev <!nocheck>,
+ librust-zstd-0.12+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.1.0-~~),
+ librust-sha2-0.10+default-dev,
+ librust-tar-0.4+default-dev,
+ librust-thiserror-1+default-dev,
+ librust-zstd-0.12+bindgen-dev,
+ librust-zstd-0.12+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..cc5a1d46
--- /dev/null
+++ b/proxmox-oci/src/lib.rs
@@ -0,0 +1,196 @@
+use flate2::read::GzDecoder;
+use oci_spec::image::{Arch, Config, ImageConfiguration, ImageManifest, MediaType};
+use oci_spec::OciSpecError;
+use oci_tar_image::OciTarImage;
+use sha2::digest::generic_array::GenericArray;
+use sha2::{Digest, Sha256};
+use std::collections::HashMap;
+use std::fs::File;
+use std::io::{Read, Seek};
+use std::path::PathBuf;
+use std::str::FromStr;
+use tar::Archive;
+use thiserror::Error;
+
+pub mod oci_tar_image;
+
+fn compute_digest<R: Read, H: Digest>(
+ mut reader: R,
+ mut hasher: H,
+) -> GenericArray<u8, H::OutputSize> {
+ let mut buf = proxmox_io::boxed::zeroed(4096);
+
+ loop {
+ let bytes_read = reader.read(&mut buf).unwrap();
+ if bytes_read == 0 {
+ break hasher.finalize();
+ }
+
+ hasher.update(&buf[..bytes_read]);
+ }
+}
+
+fn compute_sha256<R: Read>(reader: R) -> oci_spec::image::Sha256Digest {
+ let digest = compute_digest(reader, Sha256::new());
+ oci_spec::image::Sha256Digest::from_str(&format!("{:x}", digest)).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>(
+ oci_tar_image: &mut OciTarImage<R>,
+ image_manifest: &ImageManifest,
+) -> HashMap<oci_spec::image::Digest, oci_spec::image::Descriptor> {
+ let mut layer_mapping = HashMap::new();
+
+ for layer in image_manifest.layers() {
+ let digest = match layer.media_type() {
+ MediaType::ImageLayer | MediaType::ImageLayerNonDistributable => {
+ Some(layer.digest().clone())
+ }
+ MediaType::ImageLayerGzip | MediaType::ImageLayerNonDistributableGzip => {
+ let compressed_blob = oci_tar_image.open_blob(layer.digest()).unwrap();
+ let decoder = GzDecoder::new(compressed_blob);
+ Some(compute_sha256(decoder).into())
+ }
+ MediaType::ImageLayerZstd | MediaType::ImageLayerNonDistributableZstd => {
+ let compressed_blob = oci_tar_image.open_blob(layer.digest()).unwrap();
+ let decoder = zstd::Decoder::new(compressed_blob).unwrap();
+ Some(compute_sha256(decoder).into())
+ }
+ _ => None,
+ };
+
+ if let Some(digest) = digest {
+ layer_mapping.insert(digest, layer.clone());
+ }
+ }
+
+ 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(
+ oci_tar_path: &str,
+ rootfs_path: &str,
+) -> Result<Option<Config>, ProxmoxOciError> {
+ let (mut oci_tar_image, image_manifest, image_config) = parse_image(oci_tar_path)?;
+
+ extract_image_rootfs(
+ &mut oci_tar_image,
+ &image_manifest,
+ &image_config,
+ rootfs_path.into(),
+ )?;
+
+ Ok(image_config.config().clone())
+}
+
+#[derive(Debug, Error)]
+pub enum ParseError {
+ #[error("Not an OCI image: {0}")]
+ NotAnOciImage(OciSpecError),
+ #[error("Wrong media type")]
+ WrongMediaType,
+ #[error("IO error: {0}")]
+ IoError(#[from] std::io::Error),
+ #[error("Unsupported CPU architecture")]
+ UnsupportedArchitecture,
+ #[error("Missing image config")]
+ MissingImageConfig,
+}
+
+impl From<OciSpecError> for ParseError {
+ fn from(oci_spec_err: OciSpecError) -> Self {
+ match oci_spec_err {
+ OciSpecError::Io(ioerr) => Self::IoError(ioerr),
+ ocierr => Self::NotAnOciImage(ocierr),
+ }
+ }
+}
+
+fn parse_image(
+ oci_tar_path: &str,
+) -> 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 image_config_file = oci_tar_image
+ .open_blob(image_config_descriptor.digest())
+ .ok_or(ParseError::MissingImageConfig)?;
+ let image_config = ImageConfiguration::from_reader(image_config_file)?;
+
+ Ok((oci_tar_image, image_manifest, image_config))
+}
+
+#[derive(Debug, Error)]
+pub enum ExtractError {
+ #[error("Rootfs destination path not found")]
+ RootfsDestinationNotFound,
+ #[error("Unknown layer digest found in rootfs.diff_ids")]
+ UnknownLayerDigest,
+ #[error("Layer file mentioned in image manifest is missing")]
+ MissingLayerFile,
+ #[error("IO error: {0}")]
+ IoError(#[from] std::io::Error),
+ #[error("Layer has wrong media type")]
+ WrongMediaType,
+}
+
+fn extract_image_rootfs<R: Read + Seek>(
+ oci_tar_image: &mut OciTarImage<R>,
+ image_manifest: &ImageManifest,
+ image_config: &ImageConfiguration,
+ target_path: PathBuf,
+) -> Result<(), ExtractError> {
+ if !target_path.exists() {
+ return Err(ExtractError::RootfsDestinationNotFound);
+ }
+
+ let 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::UnknownLayerDigest)?;
+ let layer_descriptor = layer_map
+ .get(&layer_digest)
+ .ok_or(ExtractError::UnknownLayerDigest)?;
+ let layer_file = oci_tar_image
+ .open_blob(layer_descriptor.digest())
+ .ok_or(ExtractError::MissingLayerFile)?;
+
+ let tar_file: Box<dyn Read> = match layer_descriptor.media_type() {
+ MediaType::ImageLayer | MediaType::ImageLayerNonDistributable => Box::new(layer_file),
+ MediaType::ImageLayerGzip | MediaType::ImageLayerNonDistributableGzip => {
+ Box::new(GzDecoder::new(layer_file))
+ }
+ MediaType::ImageLayerZstd | MediaType::ImageLayerNonDistributableZstd => {
+ Box::new(zstd::Decoder::new(layer_file)?)
+ }
+ _ => return Err(ExtractError::WrongMediaType),
+ };
+
+ let mut archive = Archive::new(tar_file);
+ archive.set_preserve_ownerships(true);
+ archive.unpack(&target_path)?;
+ }
+
+ Ok(())
+}
diff --git a/proxmox-oci/src/oci_tar_image.rs b/proxmox-oci/src/oci_tar_image.rs
new file mode 100644
index 00000000..555558f5
--- /dev/null
+++ b/proxmox-oci/src/oci_tar_image.rs
@@ -0,0 +1,167 @@
+use oci_spec::image::{Arch, Digest, ImageIndex, ImageManifest, MediaType};
+use oci_spec::OciSpecError;
+use std::cmp::min;
+use std::collections::HashMap;
+use std::io::{Read, Seek, SeekFrom};
+use std::path::PathBuf;
+use tar::Archive;
+
+#[derive(Clone)]
+pub struct TarEntry {
+ offset: u64,
+ size: usize,
+}
+
+pub struct TarEntryReader<'a, R: Read + Seek> {
+ tar_entry: TarEntry,
+ reader: &'a mut R,
+ position: u64,
+}
+
+impl<'a, R: Read + Seek> TarEntryReader<'a, R> {
+ pub fn new(tar_entry: TarEntry, reader: &'a mut R) -> Self {
+ Self {
+ tar_entry,
+ reader,
+ position: 0,
+ }
+ }
+}
+
+impl<R: Read + Seek> Read for TarEntryReader<'_, R> {
+ fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
+ let max_read = min(buf.len(), self.tar_entry.size - self.position as usize);
+ let limited_buf = &mut buf[..max_read];
+ self.reader
+ .seek(SeekFrom::Start(self.tar_entry.offset + self.position))?;
+ let bytes_read = self.reader.read(limited_buf)?;
+ self.position += bytes_read as u64;
+
+ Ok(bytes_read)
+ }
+}
+
+impl<R: Read + Seek> Seek for TarEntryReader<'_, R> {
+ fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
+ self.position = match pos {
+ SeekFrom::Start(position) => min(position, self.tar_entry.size as u64),
+ SeekFrom::End(offset) => {
+ if offset > self.tar_entry.size as i64 {
+ return Err(std::io::Error::new(
+ std::io::ErrorKind::NotSeekable,
+ "Tried to seek before the beginning of the file",
+ ));
+ }
+
+ (if offset <= 0 {
+ self.tar_entry.size
+ } else {
+ self.tar_entry.size - offset as usize
+ }) as u64
+ }
+ SeekFrom::Current(offset) => {
+ if (self.position as i64 + offset) < 0 {
+ return Err(std::io::Error::new(
+ std::io::ErrorKind::NotSeekable,
+ "Tried to seek before the beginning of the file",
+ ));
+ }
+
+ min(self.position + offset as u64, self.tar_entry.size as u64)
+ }
+ };
+
+ Ok(self.position)
+ }
+}
+
+struct TarArchive<R: Read + Seek> {
+ reader: R,
+ entries: HashMap<PathBuf, TarEntry>,
+}
+
+impl<R: Read + Seek> TarArchive<R> {
+ pub fn new(reader: R) -> std::io::Result<Self> {
+ let mut archive = Archive::new(reader);
+ let entries = archive.entries_with_seek()?;
+ let mut entries_index = HashMap::new();
+
+ for entry in entries {
+ let entry = entry?;
+ let offset = entry.raw_file_position();
+ let size = entry.size() as usize;
+ let path = entry.path()?.into_owned();
+ let tar_entry = TarEntry { offset, size };
+ entries_index.insert(path, tar_entry);
+ }
+
+ Ok(Self {
+ reader: archive.into_inner(),
+ entries: entries_index,
+ })
+ }
+
+ pub fn get_inner_file(&mut self, inner_file_path: PathBuf) -> Option<TarEntryReader<R>> {
+ match self.entries.get(&inner_file_path) {
+ Some(tar_entry) => Some(TarEntryReader::new(tar_entry.clone(), &mut self.reader)),
+ None => None,
+ }
+ }
+}
+
+pub struct OciTarImage<R: Read + Seek> {
+ archive: TarArchive<R>,
+ image_index: ImageIndex,
+}
+
+impl<R: Read + Seek> OciTarImage<R> {
+ pub fn new(reader: R) -> oci_spec::Result<Self> {
+ let mut archive = TarArchive::new(reader)?;
+ let index_file = archive
+ .get_inner_file("index.json".into())
+ .ok_or(OciSpecError::Other("Missing index.json file".into()))?;
+ let image_index = ImageIndex::from_reader(index_file)?;
+
+ Ok(Self {
+ archive,
+ image_index,
+ })
+ }
+
+ pub fn image_index(&self) -> &ImageIndex {
+ &self.image_index
+ }
+
+ pub fn open_blob(&mut self, digest: &Digest) -> Option<TarEntryReader<R>> {
+ self.archive.get_inner_file(get_blob_path(digest))
+ }
+
+ pub fn image_manifest(
+ &mut self,
+ architecture: &Arch,
+ ) -> Option<oci_spec::Result<ImageManifest>> {
+ 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) => match self.open_blob(&descriptor.digest().clone()) {
+ Some(image_manifest_file) => Some(ImageManifest::from_reader(image_manifest_file)),
+ None => Some(Err(OciSpecError::Other(
+ "Image manifest mentioned in image index is missing".into(),
+ ))),
+ },
+ None => None,
+ }
+ }
+}
+
+fn get_blob_path(digest: &Digest) -> PathBuf {
+ let algorithm = digest.algorithm().as_ref();
+ let digest = digest.digest();
+ let mut path = PathBuf::from("blobs");
+ path.push(algorithm);
+ path.push(digest);
+ path
+}
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 29+ messages in thread
* [pve-devel] [PATCH proxmox-perl-rs v2 02/11] add Perl mapping for OCI container image parser/extractor
2025-06-11 14:48 [pve-devel] [PATCH container/manager/proxmox{, -perl-rs}/storage v2 00/11] support OCI images as container templates Filip Schauer
2025-06-11 14:48 ` [pve-devel] [PATCH proxmox v2 01/11] add proxmox-oci crate Filip Schauer
@ 2025-06-11 14:48 ` Filip Schauer
2025-06-24 12:51 ` Wolfgang Bumiller
2025-06-11 14:48 ` [pve-devel] [PATCH container v2 03/11] config: whitelist lxc.init.cwd Filip Schauer
` (10 subsequent siblings)
12 siblings, 1 reply; 29+ messages in thread
From: Filip Schauer @ 2025-06-11 14:48 UTC (permalink / raw)
To: pve-devel
Signed-off-by: Filip Schauer <f.schauer@proxmox.com>
---
pve-rs/Cargo.toml | 2 ++
pve-rs/Makefile | 1 +
pve-rs/debian/control | 2 ++
pve-rs/src/lib.rs | 1 +
pve-rs/src/oci.rs | 20 ++++++++++++++++++++
5 files changed, 26 insertions(+)
create mode 100644 pve-rs/src/oci.rs
diff --git a/pve-rs/Cargo.toml b/pve-rs/Cargo.toml
index c7f11a3..00624c3 100644
--- a/pve-rs/Cargo.toml
+++ b/pve-rs/Cargo.toml
@@ -20,6 +20,7 @@ hex = "0.4"
http = "1"
libc = "0.2"
nix = "0.29"
+oci-spec = "0.8.1"
openssl = "0.10.40"
serde = "1.0"
serde_bytes = "0.11"
@@ -37,6 +38,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 afe792a..d6a667f 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..243b428 100644
--- a/pve-rs/debian/control
+++ b/pve-rs/debian/control
@@ -10,6 +10,7 @@ Build-Depends: cargo:native <!nocheck>,
librust-http-1+default-dev (>= 0.2.7-~~),
librust-libc-0.2+default-dev,
librust-nix-0.29+default-dev,
+ librust-oci-spec-0.8+default-dev (>= 0.8.1-~~),
librust-openssl-0.10+default-dev (>= 0.10.40-~~),
librust-perlmod-0.14+default-dev (>= 0.13.5-~~),
librust-perlmod-0.14+exporter-dev (>= 0.13.5-~~),
@@ -24,6 +25,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/lib.rs b/pve-rs/src/lib.rs
index bb979b9..1fe9e77 100644
--- a/pve-rs/src/lib.rs
+++ b/pve-rs/src/lib.rs
@@ -12,6 +12,7 @@ use proxmox_notify::{Config, Notification, Severity};
mod common;
pub mod firewall;
+pub mod oci;
pub mod openid;
pub mod bindings;
diff --git a/pve-rs/src/oci.rs b/pve-rs/src/oci.rs
new file mode 100644
index 0000000..dd556aa
--- /dev/null
+++ b/pve-rs/src/oci.rs
@@ -0,0 +1,20 @@
+#[perlmod::package(name = "PVE::RS::OCI")]
+mod export {
+ use anyhow::{Error, bail};
+ use oci_spec::image::Config;
+ use proxmox_oci::{ParseError, ProxmoxOciError};
+
+ #[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) => match err {
+ ProxmoxOciError::ParseError(ParseError::NotAnOciImage(_)) => Ok(None),
+ err => bail!("{err}"),
+ },
+ }
+ }
+}
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 29+ messages in thread
* [pve-devel] [PATCH container v2 03/11] config: whitelist lxc.init.cwd
2025-06-11 14:48 [pve-devel] [PATCH container/manager/proxmox{, -perl-rs}/storage v2 00/11] support OCI images as container templates Filip Schauer
2025-06-11 14:48 ` [pve-devel] [PATCH proxmox v2 01/11] add proxmox-oci crate Filip Schauer
2025-06-11 14:48 ` [pve-devel] [PATCH proxmox-perl-rs v2 02/11] add Perl mapping for OCI container image parser/extractor Filip Schauer
@ 2025-06-11 14:48 ` Filip Schauer
2025-06-25 9:00 ` [pve-devel] applied: " Wolfgang Bumiller
2025-06-11 14:48 ` [pve-devel] [PATCH container v2 04/11] add support for OCI images as container templates Filip Schauer
` (9 subsequent siblings)
12 siblings, 1 reply; 29+ messages in thread
From: Filip Schauer @ 2025-06-11 14:48 UTC (permalink / raw)
To: pve-devel
This parameter allows setting the working directory of the init process
in the container. This can be used by containers created from an OCI
image, that specifies a custom working directory.
Signed-off-by: Filip Schauer <f.schauer@proxmox.com>
---
src/PVE/LXC/Config.pm | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/PVE/LXC/Config.pm b/src/PVE/LXC/Config.pm
index 0740e8c..49067ea 100644
--- a/src/PVE/LXC/Config.pm
+++ b/src/PVE/LXC/Config.pm
@@ -641,6 +641,7 @@ my $valid_lxc_conf_keys = {
'lxc.signal.reboot' => 1,
'lxc.signal.stop' => 1,
'lxc.init.cmd' => 1,
+ 'lxc.init.cwd' => 1,
'lxc.pty.max' => 1,
'lxc.console.logfile' => 1,
'lxc.console.path' => 1,
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 29+ messages in thread
* [pve-devel] [PATCH container v2 04/11] add support for OCI images as container templates
2025-06-11 14:48 [pve-devel] [PATCH container/manager/proxmox{, -perl-rs}/storage v2 00/11] support OCI images as container templates Filip Schauer
` (2 preceding siblings ...)
2025-06-11 14:48 ` [pve-devel] [PATCH container v2 03/11] config: whitelist lxc.init.cwd Filip Schauer
@ 2025-06-11 14:48 ` Filip Schauer
2025-06-11 14:48 ` [pve-devel] [PATCH container v2 05/11] config: add entrypoint parameter Filip Schauer
` (8 subsequent siblings)
12 siblings, 0 replies; 29+ messages in thread
From: Filip Schauer @ 2025-06-11 14:48 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>
---
src/PVE/API2/LXC.pm | 64 ++++++++++++++++++++++++++++++++++++++++++---
1 file changed, 61 insertions(+), 3 deletions(-)
diff --git a/src/PVE/API2/LXC.pm b/src/PVE/API2/LXC.pm
index 5c6ee57..3b0ea35 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;
@@ -484,9 +486,65 @@ __PACKAGE__->register_method({
eval {
my $rootdir = PVE::LXC::mount_all($vmid, $storage_cfg, $conf, 1);
$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 $oci_config;
+
+ if ($restore && $archive ne '-') {
+ print "restoring '$archive' now..\n";
+ } else {
+ # Try interpreting the file as an OCI image first.
+ # If it fails, treat it as an LXC template instead.
+ my $archivepath = PVE::Storage::abs_filesystem_path($storage_cfg, $archive);
+ if ($archivepath =~ /\.tar$/) {
+ my ($id_map, $root_uid, $root_gid) = PVE::LXC::parse_id_maps($conf);
+ $oci_config = PVE::LXC::Namespaces::run_in_userns(sub {
+ PVE::RS::OCI::parse_and_extract_image($archivepath, $rootdir)
+ }, $id_map);
+ }
+ }
+
+ if (defined($oci_config)) {
+ # OCI image extracted successfully
+
+ # 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);
+ }
+ 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.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 29+ messages in thread
* [pve-devel] [PATCH container v2 05/11] config: add entrypoint parameter
2025-06-11 14:48 [pve-devel] [PATCH container/manager/proxmox{, -perl-rs}/storage v2 00/11] support OCI images as container templates Filip Schauer
` (3 preceding siblings ...)
2025-06-11 14:48 ` [pve-devel] [PATCH container v2 04/11] add support for OCI images as container templates Filip Schauer
@ 2025-06-11 14:48 ` Filip Schauer
2025-06-11 14:48 ` [pve-devel] [PATCH container v2 06/11] configure static IP in LXC config for custom entrypoint Filip Schauer
` (7 subsequent siblings)
12 siblings, 0 replies; 29+ messages in thread
From: Filip Schauer @ 2025-06-11 14:48 UTC (permalink / raw)
To: pve-devel
Signed-off-by: Filip Schauer <f.schauer@proxmox.com>
---
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 3b0ea35..cf93778 100644
--- a/src/PVE/API2/LXC.pm
+++ b/src/PVE/API2/LXC.pm
@@ -517,7 +517,7 @@ __PACKAGE__->register_method({
my $s = $_; $s =~ s/"/\\"/g; qq{"$_"}
} @init_cmd);
}
- 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
$conf->{cmode} = 'console';
diff --git a/src/PVE/LXC.pm b/src/PVE/LXC.pm
index 2b9f0cf..b2be27e 100644
--- a/src/PVE/LXC.pm
+++ b/src/PVE/LXC.pm
@@ -794,6 +794,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 49067ea..d7d8b6a 100644
--- a/src/PVE/LXC/Config.pm
+++ b/src/PVE/LXC/Config.pm
@@ -591,6 +591,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',
@@ -1745,6 +1751,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.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 29+ messages in thread
* [pve-devel] [PATCH container v2 06/11] configure static IP in LXC config for custom entrypoint
2025-06-11 14:48 [pve-devel] [PATCH container/manager/proxmox{, -perl-rs}/storage v2 00/11] support OCI images as container templates Filip Schauer
` (4 preceding siblings ...)
2025-06-11 14:48 ` [pve-devel] [PATCH container v2 05/11] config: add entrypoint parameter Filip Schauer
@ 2025-06-11 14:48 ` Filip Schauer
2025-06-25 8:26 ` Wolfgang Bumiller
2025-06-11 14:48 ` [pve-devel] [PATCH container v2 07/11] setup: debian: create /etc/network path if missing Filip Schauer
` (6 subsequent siblings)
12 siblings, 1 reply; 29+ messages in thread
From: Filip Schauer @ 2025-06-11 14:48 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>
---
src/PVE/LXC.pm | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/src/PVE/LXC.pm b/src/PVE/LXC.pm
index b2be27e..0131ac3 100644
--- a/src/PVE/LXC.pm
+++ b/src/PVE/LXC.pm
@@ -818,6 +818,18 @@ 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
+ && PVE::LXC::Config->get_entrypoint($conf) ne "/sbin/init"
+ ) {
+ $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});
+ $raw .= "lxc.net.$ind.ipv6.address = $d->{ip6}\n"
+ if defined($d->{ip6}) && $d->{ip6} !~ /^(auto|dhcp|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;
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 29+ messages in thread
* [pve-devel] [PATCH container v2 07/11] setup: debian: create /etc/network path if missing
2025-06-11 14:48 [pve-devel] [PATCH container/manager/proxmox{, -perl-rs}/storage v2 00/11] support OCI images as container templates Filip Schauer
` (5 preceding siblings ...)
2025-06-11 14:48 ` [pve-devel] [PATCH container v2 06/11] configure static IP in LXC config for custom entrypoint Filip Schauer
@ 2025-06-11 14:48 ` Filip Schauer
2025-06-11 14:49 ` [pve-devel] [PATCH container v2 08/11] setup: recursively mkdir /etc/systemd/{network, system-preset} Filip Schauer
` (5 subsequent siblings)
12 siblings, 0 replies; 29+ messages in thread
From: Filip Schauer @ 2025-06-11 14:48 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 8f7f524..b607c44 100644
--- a/src/PVE/LXC/Setup/Debian.pm
+++ b/src/PVE/LXC/Setup/Debian.pm
@@ -418,6 +418,7 @@ sub setup_network {
$interfaces;
}
+ $self->ct_make_path('/etc/network');
$self->ct_file_set_contents($filename, $interfaces);
}
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 29+ messages in thread
* [pve-devel] [PATCH container v2 08/11] setup: recursively mkdir /etc/systemd/{network, system-preset}
2025-06-11 14:48 [pve-devel] [PATCH container/manager/proxmox{, -perl-rs}/storage v2 00/11] support OCI images as container templates Filip Schauer
` (6 preceding siblings ...)
2025-06-11 14:48 ` [pve-devel] [PATCH container v2 07/11] setup: debian: create /etc/network path if missing Filip Schauer
@ 2025-06-11 14:49 ` Filip Schauer
2025-06-11 14:49 ` [pve-devel] [PATCH container v2 09/11] manage DHCP for containers with custom entrypoint Filip Schauer
` (4 subsequent siblings)
12 siblings, 0 replies; 29+ messages in thread
From: Filip Schauer @ 2025-06-11 14:49 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>
---
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 bb084af..7119e90 100644
--- a/src/PVE/LXC/Setup/Base.pm
+++ b/src/PVE/LXC/Setup/Base.pm
@@ -306,6 +306,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);
}
}
@@ -345,7 +346,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.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 29+ messages in thread
* [pve-devel] [PATCH container v2 09/11] manage DHCP for containers with custom entrypoint
2025-06-11 14:48 [pve-devel] [PATCH container/manager/proxmox{, -perl-rs}/storage v2 00/11] support OCI images as container templates Filip Schauer
` (7 preceding siblings ...)
2025-06-11 14:49 ` [pve-devel] [PATCH container v2 08/11] setup: recursively mkdir /etc/systemd/{network, system-preset} Filip Schauer
@ 2025-06-11 14:49 ` Filip Schauer
2025-06-25 8:50 ` Wolfgang Bumiller
2025-06-11 14:49 ` [pve-devel] [PATCH storage v2 10/11] allow .tar container templates Filip Schauer
` (3 subsequent siblings)
12 siblings, 1 reply; 29+ messages in thread
From: Filip Schauer @ 2025-06-11 14:49 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>
---
src/PVE/LXC.pm | 74 ++++++++++++++++++++++++++++++++++++++++---
src/PVE/LXC/Config.pm | 6 +++-
2 files changed, 74 insertions(+), 6 deletions(-)
diff --git a/src/PVE/LXC.pm b/src/PVE/LXC.pm
index 0131ac3..e91b53a 100644
--- a/src/PVE/LXC.pm
+++ b/src/PVE/LXC.pm
@@ -1004,6 +1004,8 @@ sub vm_stop_cleanup {
PVE::Storage::deactivate_volumes($storage_cfg, $vollist);
};
warn $@ if $@; # avoid errors - just warn
+
+ kill_dhclients($vmid, '*') if (PVE::LXC::Config->get_entrypoint($conf) ne "/sbin/init");
}
sub net_tap_plug : prototype($$) {
@@ -1189,6 +1191,34 @@ 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',
+ '/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;
+ kill 9, $pid if ($pid =~ m/^\d+$/);
+ unlink($pidfile);
+ }
+ }
+}
+
sub update_ipconfig {
my ($vmid, $conf, $opt, $eth, $newnet, $rootdir) = @_;
@@ -1223,11 +1253,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 (PVE::LXC::Config->get_entrypoint($conf) ne "/sbin/init") {
+ 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;
+ }
}
}
@@ -2707,6 +2747,30 @@ sub vm_start {
}
PVE::GuestHelpers::exec_hookscript($conf, $vmid, 'post-start');
+ 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";
+
+ if (PVE::LXC::Config->get_entrypoint($conf) ne "/sbin/init") {
+ 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('stop', $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 d7d8b6a..854e711 100644
--- a/src/PVE/LXC/Config.pm
+++ b/src/PVE/LXC/Config.pm
@@ -1490,9 +1490,13 @@ sub vmconfig_hotplug_pending {
$cgroup->change_cpu_shares(undef);
} elsif ($opt =~ m/^net(\d)$/) {
my $netid = $1;
+ my $net = PVE::LXC::Config->parse_lxc_network($conf->{$opt});
+ if (PVE::LXC::Config->get_entrypoint($conf) ne "/sbin/init") {
+ PVE::LXC::kill_dhclients($vmid, $net->{name});
+ }
+
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($net->{bridge}, $net->{hwaddr}, $conf->{hostname}) };
warn $@ if $@;
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 29+ messages in thread
* [pve-devel] [PATCH storage v2 10/11] allow .tar container templates
2025-06-11 14:48 [pve-devel] [PATCH container/manager/proxmox{, -perl-rs}/storage v2 00/11] support OCI images as container templates Filip Schauer
` (8 preceding siblings ...)
2025-06-11 14:49 ` [pve-devel] [PATCH container v2 09/11] manage DHCP for containers with custom entrypoint Filip Schauer
@ 2025-06-11 14:49 ` Filip Schauer
2025-06-24 13:11 ` Wolfgang Bumiller
2025-06-11 14:49 ` [pve-devel] [PATCH manager v2 11/11] ui: storage upload: accept *.tar files as vztmpl Filip Schauer
` (2 subsequent siblings)
12 siblings, 1 reply; 29+ messages in thread
From: Filip Schauer @ 2025-06-11 14:49 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>
---
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 3962077..0d43df1 100755
--- a/src/PVE/Storage.pm
+++ b/src/PVE/Storage.pm
@@ -117,7 +117,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(?:\.(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 1d8a8f9..76ffdb6 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 ? "t$2" : "tar" };
} elsif ($tt eq 'backup') {
next if $fn !~ m!/([^/]+$PVE::Storage::BACKUP_EXT_RE_2)$!;
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 29+ messages in thread
* [pve-devel] [PATCH manager v2 11/11] ui: storage upload: accept *.tar files as vztmpl
2025-06-11 14:48 [pve-devel] [PATCH container/manager/proxmox{, -perl-rs}/storage v2 00/11] support OCI images as container templates Filip Schauer
` (9 preceding siblings ...)
2025-06-11 14:49 ` [pve-devel] [PATCH storage v2 10/11] allow .tar container templates Filip Schauer
@ 2025-06-11 14:49 ` Filip Schauer
2025-06-17 8:01 ` [pve-devel] [PATCH container/manager/proxmox{, -perl-rs}/storage v2 00/11] support OCI images as container templates Christoph Heiss
2025-07-09 12:40 ` [pve-devel] superseded: " Filip Schauer
12 siblings, 0 replies; 29+ messages in thread
From: Filip Schauer @ 2025-06-11 14:49 UTC (permalink / raw)
To: pve-devel
Signed-off-by: Filip Schauer <f.schauer@proxmox.com>
---
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 f6cad0ec..cfd0a949 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.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 29+ messages in thread
* Re: [pve-devel] [PATCH container/manager/proxmox{, -perl-rs}/storage v2 00/11] support OCI images as container templates
2025-06-11 14:48 [pve-devel] [PATCH container/manager/proxmox{, -perl-rs}/storage v2 00/11] support OCI images as container templates Filip Schauer
` (10 preceding siblings ...)
2025-06-11 14:49 ` [pve-devel] [PATCH manager v2 11/11] ui: storage upload: accept *.tar files as vztmpl Filip Schauer
@ 2025-06-17 8:01 ` Christoph Heiss
2025-07-09 12:50 ` Filip Schauer
2025-07-09 12:40 ` [pve-devel] superseded: " Filip Schauer
12 siblings, 1 reply; 29+ messages in thread
From: Christoph Heiss @ 2025-06-17 8:01 UTC (permalink / raw)
To: Filip Schauer; +Cc: Proxmox VE development discussion
pve-container changes now need a rebase due to the perltidy
re-formatting.
Tested the series using the same setup as last time. Tested the
following images, exported with podman:
- docker.io/library/nginx:mainline-alpine
- docker.io/library/nginx:mainline-bookworm
- ghcr.io/dani-garcia/vaultwarden:1.34.1-alpine
- docker.io/library/node:24-bookworm
Can confirm that all of these started up correctly and are
reachable/usable afterwards and that the problem with the entrypoint has
been fixed.
I also test with `ghcr.io/nixos/nix:latest`, which interestingly fails
to start with
DEBUG utils - ../src/lxc/utils.c:run_buffer:560 - Script exec /usr/share/lxcfs/lxc.mount.hook 107 lxc mount produced output: /usr/share/lxcfs/lxc.mount.hook: 15: readlink: Permission denied
Not sure what is going on there, but I don't think it's directly related
to this series, rather just some OCI/Nix weirdness.
On Wed Jun 11, 2025 at 4:48 PM CEST, Filip Schauer wrote:
> Add basic support for OCI (Open Container Initiative) images [0] as
> container templates.
>
> An OCI image can be for example obtained from Docker Hub:
>
> Either using Docker:
>
> ```
> $ docker pull httpd
> $ docker save httpd > httpd.tar
> ```
>
> Or using Podman:
> When using Podman, the format needs to be explicitly specified,
> otherwise it defaults to docker-archive.
>
> ```
> $ podman pull httpd
> $ podman save --format=oci-archive httpd > httpd.tar
> ```
>
> The tarball can be uploaded to a storage as a container template and
> then used during container creation. It is automatically detected that
> the container template is an OCI image. The resulting container still
> uses the existing LXC framework.
Needs to documented in pve-docs too, this paragraph would already make a
great start tbh.
>
[..]
> # Build & install order:
>
> OCI image support:
> 1. proxmox
> 2. proxmox-perl-rs
> 3. pve-container
>
> .tar container template support:
> 1. pve-storage
> 2. pve-manager
Should be mentioned here (and ideally, on the respective pve-container
patch(es) as well) that pve-container depends on libpve-rs-perl after
this series, i.e. an appropriate entry needs to be added to d/control.
>
> [0] https://github.com/opencontainers/image-spec/blob/main/spec.md
> [1] https://lore.proxmox.com/pve-devel/20250606103719.533030-2-c.heiss@proxmox.com/
>
> Changed since v1:
> * Fix entrypoint command missing Cmd
> * Set lxc.signal.halt according to StopSignal (Fixes container shutdown)
> * setup: Ensure that both /etc/systemd/network and
> /etc/systemd/system-preset exist before writing files into them.
> * ui: storage upload: accept *.tar files as vztmpl
> * proxmox-perl-rs: rebase on latest master (3d9806cb3c7f)
> * proxmox-perl-rs: add new dependencies to debian/control
> * proxmox-oci: refactor errors and use `thiserror` to avoid boilerplate
Please also mention changes in the notes of each patch - makes reviewing
a lot easier!
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 29+ messages in thread
* Re: [pve-devel] [PATCH proxmox v2 01/11] add proxmox-oci crate
2025-06-11 14:48 ` [pve-devel] [PATCH proxmox v2 01/11] add proxmox-oci crate Filip Schauer
@ 2025-06-24 12:42 ` Wolfgang Bumiller
2025-06-25 8:13 ` Wolfgang Bumiller
1 sibling, 0 replies; 29+ messages in thread
From: Wolfgang Bumiller @ 2025-06-24 12:42 UTC (permalink / raw)
To: Filip Schauer; +Cc: pve-devel
On Wed, Jun 11, 2025 at 04:48:53PM +0200, Filip Schauer wrote:
> This crate can parse and extract an OCI image bundled as a tar archive.
>
> Signed-off-by: Filip Schauer <f.schauer@proxmox.com>
> ---
> Cargo.toml | 1 +
> proxmox-oci/Cargo.toml | 22 ++++
> proxmox-oci/debian/changelog | 5 +
> proxmox-oci/debian/control | 47 ++++++++
> proxmox-oci/debian/debcargo.toml | 7 ++
> proxmox-oci/src/lib.rs | 196 +++++++++++++++++++++++++++++++
> proxmox-oci/src/oci_tar_image.rs | 167 ++++++++++++++++++++++++++
> 7 files changed, 445 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 bf9e83d7..8365b18a 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-rest-server",
> 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..3974cf48
> --- /dev/null
> +++ b/proxmox-oci/debian/control
> @@ -0,0 +1,47 @@
> +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.1.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.12+bindgen-dev <!nocheck>,
> + librust-zstd-0.12+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.1.0-~~),
> + librust-sha2-0.10+default-dev,
> + librust-tar-0.4+default-dev,
> + librust-thiserror-1+default-dev,
> + librust-zstd-0.12+bindgen-dev,
> + librust-zstd-0.12+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..cc5a1d46
> --- /dev/null
> +++ b/proxmox-oci/src/lib.rs
> @@ -0,0 +1,196 @@
> +use flate2::read::GzDecoder;
> +use oci_spec::image::{Arch, Config, ImageConfiguration, ImageManifest, MediaType};
> +use oci_spec::OciSpecError;
> +use oci_tar_image::OciTarImage;
^ Please group this import with its `mod`.
> +use sha2::digest::generic_array::GenericArray;
> +use sha2::{Digest, Sha256};
> +use std::collections::HashMap;
> +use std::fs::File;
> +use std::io::{Read, Seek};
> +use std::path::PathBuf;
> +use std::str::FromStr;
> +use tar::Archive;
> +use thiserror::Error;
^ Please group imports. `std` first, the rest by "distance":
- std
- external
- ours
- `crate::...`
> +
> +pub mod oci_tar_image;
^ Does it make sense to make this public?
(Generally: avoid using `pub` and only add it where needed.)
> +
> +fn compute_digest<R: Read, H: Digest>(
> + mut reader: R,
> + mut hasher: H,
> +) -> GenericArray<u8, H::OutputSize> {
> + let mut buf = proxmox_io::boxed::zeroed(4096);
> +
> + loop {
> + let bytes_read = reader.read(&mut buf).unwrap();
^ This `unwrap()` is reachable through regular file I/O and potentially
compression layers AFAICT... We definitely need to propagate errors
here.
> + if bytes_read == 0 {
> + break hasher.finalize();
> + }
> +
> + hasher.update(&buf[..bytes_read]);
> + }
> +}
> +
> +fn compute_sha256<R: Read>(reader: R) -> oci_spec::image::Sha256Digest {
> + let digest = compute_digest(reader, Sha256::new());
> + oci_spec::image::Sha256Digest::from_str(&format!("{:x}", digest)).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>(
> + oci_tar_image: &mut OciTarImage<R>,
> + image_manifest: &ImageManifest,
> +) -> HashMap<oci_spec::image::Digest, oci_spec::image::Descriptor> {
> + let mut layer_mapping = HashMap::new();
> +
> + for layer in image_manifest.layers() {
> + let digest = match layer.media_type() {
> + MediaType::ImageLayer | MediaType::ImageLayerNonDistributable => {
> + Some(layer.digest().clone())
> + }
> + MediaType::ImageLayerGzip | MediaType::ImageLayerNonDistributableGzip => {
> + let compressed_blob = oci_tar_image.open_blob(layer.digest()).unwrap();
> + let decoder = GzDecoder::new(compressed_blob);
> + Some(compute_sha256(decoder).into())
> + }
> + MediaType::ImageLayerZstd | MediaType::ImageLayerNonDistributableZstd => {
> + let compressed_blob = oci_tar_image.open_blob(layer.digest()).unwrap();
> + let decoder = zstd::Decoder::new(compressed_blob).unwrap();
> + Some(compute_sha256(decoder).into())
> + }
> + _ => None,
This could just `continue` and the match can then drop the `Option` and
we can skip the `if let Some()` below.
Also:
Are other types not important? If so, why? (This should be documented.)
And do we know that this stays the case, or should we list the rest
explicitly, so that `oci-spec` crate updates force use to look at that?
> + };
> +
> + if let Some(digest) = digest {
> + layer_mapping.insert(digest, layer.clone());
> + }
> + }
> +
> + 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(
> + oci_tar_path: &str,
> + rootfs_path: &str,
Use `&Path` or `P: AsRef<Path>` for these parameters.
> +) -> Result<Option<Config>, ProxmoxOciError> {
> + let (mut oci_tar_image, image_manifest, image_config) = parse_image(oci_tar_path)?;
> +
> + extract_image_rootfs(
> + &mut oci_tar_image,
> + &image_manifest,
> + &image_config,
> + rootfs_path.into(),
> + )?;
> +
> + Ok(image_config.config().clone())
> +}
> +
> +#[derive(Debug, Error)]
> +pub enum ParseError {
> + #[error("Not an OCI image: {0}")]
> + NotAnOciImage(OciSpecError),
^ I'm not convinced this is the right name, as in I don't think mapping
all `OciSpecError::SerDe` and `...::Builder` errors to "this is not an
OCI image" makes sense.
Just use `OciSpec(OciSpecError)`.
> + #[error("Wrong media type")]
> + WrongMediaType,
> + #[error("IO error: {0}")]
> + IoError(#[from] std::io::Error),
^ I'd drop the `Error` suffix since we're already in the
`Parse*Error*` type anyway ;-)
(The other variants don't have an Error suffix either, after all).
> + #[error("Unsupported CPU architecture")]
> + UnsupportedArchitecture,
> + #[error("Missing image config")]
> + MissingImageConfig,
> +}
> +
> +impl From<OciSpecError> for ParseError {
> + fn from(oci_spec_err: OciSpecError) -> Self {
> + match oci_spec_err {
> + OciSpecError::Io(ioerr) => Self::IoError(ioerr),
> + ocierr => Self::NotAnOciImage(ocierr),
^ If we decide that `NotAnOciImage` is not the correct name, I'd also
drop this `From` impl and just use `#[from]`, since then it would be
nice to have the IO error in the place where it happens I think...
> + }
> + }
> +}
> +
> +fn parse_image(
> + oci_tar_path: &str,
> +) -> 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 image_config_file = oci_tar_image
> + .open_blob(image_config_descriptor.digest())
> + .ok_or(ParseError::MissingImageConfig)?;
> + let image_config = ImageConfiguration::from_reader(image_config_file)?;
> +
> + Ok((oci_tar_image, image_manifest, image_config))
> +}
> +
> +#[derive(Debug, Error)]
> +pub enum ExtractError {
> + #[error("Rootfs destination path not found")]
> + RootfsDestinationNotFound,
^ Is it really that helpful to have this case at all?
If the only entry point into this crate is via the
`parse_and_extract_image()` function, we can just document that the path
should be pre-created and not check this.
> + #[error("Unknown layer digest found in rootfs.diff_ids")]
> + UnknownLayerDigest,
> + #[error("Layer file mentioned in image manifest is missing")]
> + MissingLayerFile,
^ Could consider including the digest in the above 2 cases.
> + #[error("IO error: {0}")]
> + IoError(#[from] std::io::Error),
^ I'd drop the `Error` suffix since we're already in the
`Extract*Error*` type anyway ;-)
> + #[error("Layer has wrong media type")]
> + WrongMediaType,
^ Could consider including the textual version of the type.
> +}
> +
> +fn extract_image_rootfs<R: Read + Seek>(
> + oci_tar_image: &mut OciTarImage<R>,
> + image_manifest: &ImageManifest,
> + image_config: &ImageConfiguration,
> + target_path: PathBuf,
Make this &Path and drop the & in the unpack() call.
> +) -> Result<(), ExtractError> {
> + if !target_path.exists() {
> + return Err(ExtractError::RootfsDestinationNotFound);
> + }
^ As mentioned - not sure this is too helpful.
> +
> + let 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::UnknownLayerDigest)?;
> + let layer_descriptor = layer_map
> + .get(&layer_digest)
> + .ok_or(ExtractError::UnknownLayerDigest)?;
> + let layer_file = oci_tar_image
> + .open_blob(layer_descriptor.digest())
> + .ok_or(ExtractError::MissingLayerFile)?;
> +
> + let tar_file: Box<dyn Read> = match layer_descriptor.media_type() {
> + MediaType::ImageLayer | MediaType::ImageLayerNonDistributable => Box::new(layer_file),
> + MediaType::ImageLayerGzip | MediaType::ImageLayerNonDistributableGzip => {
> + Box::new(GzDecoder::new(layer_file))
> + }
> + MediaType::ImageLayerZstd | MediaType::ImageLayerNonDistributableZstd => {
> + Box::new(zstd::Decoder::new(layer_file)?)
> + }
> + _ => return Err(ExtractError::WrongMediaType),
> + };
> +
> + let mut archive = Archive::new(tar_file);
> + archive.set_preserve_ownerships(true);
What about `.set_preserve_permissions()`, `.set_preserve_xattrs()`,
> + archive.unpack(&target_path)?;
> + }
> +
> + Ok(())
> +}
> diff --git a/proxmox-oci/src/oci_tar_image.rs b/proxmox-oci/src/oci_tar_image.rs
> new file mode 100644
> index 00000000..555558f5
> --- /dev/null
> +++ b/proxmox-oci/src/oci_tar_image.rs
> @@ -0,0 +1,167 @@
> +use oci_spec::image::{Arch, Digest, ImageIndex, ImageManifest, MediaType};
> +use oci_spec::OciSpecError;
> +use std::cmp::min;
> +use std::collections::HashMap;
> +use std::io::{Read, Seek, SeekFrom};
> +use std::path::PathBuf;
> +use tar::Archive;
^ Group these as well.
> +
> +#[derive(Clone)]
^ Could also be Copy
> +pub struct TarEntry {
> + offset: u64,
> + size: usize,
> +}
> +
> +pub struct TarEntryReader<'a, R: Read + Seek> {
^ This is quite generic. We could put this in `proxmox-io`:
But then I'd argue to drop the lifetime and take `R` directly, since
both `Read` and `Seek` are implemented for mutable references:
struct RangeReader<R: Read + Seek> {
reader: R,
/// Range inside `R`.
range: Range<u64>,
/// Relative position inside `range`.
position: u64,
/// True once we performed the initial seek.
ready: bool,
}
impl<R: Read + Seek> RangeReader<R> {
pub fn new(reader: R, range: Range<u64>) -> Self;
pub fn into_inner(self) -> R;
pub fn size(&self) -> usize;
pub fn remaining(&self) -> usize;
}
> + tar_entry: TarEntry,
> + reader: &'a mut R,
> + position: u64,
> +}
> +
> +impl<'a, R: Read + Seek> TarEntryReader<'a, R> {
> + pub fn new(tar_entry: TarEntry, reader: &'a mut R) -> Self {
^ As an *internal* helper (if not moved to proxmox-io), this `pub`
should be dropped.
> + Self {
> + tar_entry,
> + reader,
> + position: 0,
> + }
> + }
> +}
> +
> +impl<R: Read + Seek> Read for TarEntryReader<'_, R> {
> + fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
> + let max_read = min(buf.len(), self.tar_entry.size - self.position as usize);
^ For maintainability/readability, I'd prefer the `size-position` to be
factored out into an `fn remaining(&self) -> usize`.
> + let limited_buf = &mut buf[..max_read];
> + self.reader
> + .seek(SeekFrom::Start(self.tar_entry.offset + self.position))?;
^ We can reduce syscalls by doing this only once:
As long as this instance of `TarEntryReader` (or `RangeReader`) exist we
have exclusive access to `R`, since we own it (or it is a mutable
reference).
> + let bytes_read = self.reader.read(limited_buf)?;
> + self.position += bytes_read as u64;
^ If made generic in `proxmox-io`, this should use
`bytes_read.min(self.size())`, just in case `R` has a broken `read()`
implementation and returns a value larger than the buffer's size...
> +
> + Ok(bytes_read)
> + }
> +}
> +
> +impl<R: Read + Seek> Seek for TarEntryReader<'_, R> {
> + fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
> + self.position = match pos {
> + SeekFrom::Start(position) => min(position, self.tar_entry.size as u64),
> + SeekFrom::End(offset) => {
> + if offset > self.tar_entry.size as i64 {
> + return Err(std::io::Error::new(
> + std::io::ErrorKind::NotSeekable,
^ NotSeekable -> InvalidInput, see `lseek(2)`/`fseek(3)`.
> + "Tried to seek before the beginning of the file",
> + ));
> + }
> +
> + (if offset <= 0 {
> + self.tar_entry.size
> + } else {
> + self.tar_entry.size - offset as usize
> + }) as u64
> + }
> + SeekFrom::Current(offset) => {
> + if (self.position as i64 + offset) < 0 {
^ Wraparound checks are better done with
`self.position.checked_add(offset)`, which will give you an `Err` if it
wraps.
But to be honest... I think we may as well disregard this and just use
`position.saturating_add(offset).min(size)`...
For "real" files, seeking past the end is not an error after all, and
it would simply put us at EOF.
> + return Err(std::io::Error::new(
> + std::io::ErrorKind::NotSeekable,
> + "Tried to seek before the beginning of the file",
> + ));
> + }
> +
> + min(self.position + offset as u64, self.tar_entry.size as u64)
> + }
> + };
> +
> + Ok(self.position)
> + }
> +}
> +
> +struct TarArchive<R: Read + Seek> {
> + reader: R,
> + entries: HashMap<PathBuf, TarEntry>,
> +}
> +
> +impl<R: Read + Seek> TarArchive<R> {
^ The struct is private, so all fns here can drop the `pub`.
> + pub fn new(reader: R) -> std::io::Result<Self> {
> + let mut archive = Archive::new(reader);
> + let entries = archive.entries_with_seek()?;
> + let mut entries_index = HashMap::new();
> +
> + for entry in entries {
> + let entry = entry?;
> + let offset = entry.raw_file_position();
> + let size = entry.size() as usize;
> + let path = entry.path()?.into_owned();
> + let tar_entry = TarEntry { offset, size };
> + entries_index.insert(path, tar_entry);
> + }
> +
> + Ok(Self {
> + reader: archive.into_inner(),
> + entries: entries_index,
> + })
> + }
> +
> + pub fn get_inner_file(&mut self, inner_file_path: PathBuf) -> Option<TarEntryReader<R>> {
^ We don't need ownership here, just use `&Path`.
Or, if you don't want to have to call `.as_ref()` at the call site, use
`P where P: AsRef<Path>`.
> + match self.entries.get(&inner_file_path) {
> + Some(tar_entry) => Some(TarEntryReader::new(tar_entry.clone(), &mut self.reader)),
> + None => None,
> + }
> + }
> +}
> +
> +pub struct OciTarImage<R: Read + Seek> {
> + archive: TarArchive<R>,
> + image_index: ImageIndex,
> +}
> +
> +impl<R: Read + Seek> OciTarImage<R> {
> + pub fn new(reader: R) -> oci_spec::Result<Self> {
> + let mut archive = TarArchive::new(reader)?;
> + let index_file = archive
> + .get_inner_file("index.json".into())
> + .ok_or(OciSpecError::Other("Missing index.json file".into()))?;
> + let image_index = ImageIndex::from_reader(index_file)?;
> +
> + Ok(Self {
> + archive,
> + image_index,
> + })
> + }
> +
> + pub fn image_index(&self) -> &ImageIndex {
> + &self.image_index
> + }
> +
> + pub fn open_blob(&mut self, digest: &Digest) -> Option<TarEntryReader<R>> {
> + self.archive.get_inner_file(get_blob_path(digest))
> + }
> +
> + pub fn image_manifest(
> + &mut self,
> + architecture: &Arch,
> + ) -> Option<oci_spec::Result<ImageManifest>> {
> + 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) => match self.open_blob(&descriptor.digest().clone()) {
> + Some(image_manifest_file) => Some(ImageManifest::from_reader(image_manifest_file)),
> + None => Some(Err(OciSpecError::Other(
> + "Image manifest mentioned in image index is missing".into(),
Would be good to include the digest in the error.
> + ))),
> + },
> + None => None,
> + }
> + }
> +}
> +
> +fn get_blob_path(digest: &Digest) -> PathBuf {
Given that all elements here are regular strings, this could just do:
format!("blobs/{algorithm}/{digest}", algorithm = digest.algorithm()).into()
And when using `<P: AsRef<Path>>` above, you could return `String` and
drop the `.into()` at the end...
> + let algorithm = digest.algorithm().as_ref();
> + let digest = digest.digest();
> + let mut path = PathBuf::from("blobs");
> + path.push(algorithm);
> + path.push(digest);
> + path
> +}
> --
> 2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 29+ messages in thread
* Re: [pve-devel] [PATCH proxmox-perl-rs v2 02/11] add Perl mapping for OCI container image parser/extractor
2025-06-11 14:48 ` [pve-devel] [PATCH proxmox-perl-rs v2 02/11] add Perl mapping for OCI container image parser/extractor Filip Schauer
@ 2025-06-24 12:51 ` Wolfgang Bumiller
2025-06-25 7:59 ` Filip Schauer
0 siblings, 1 reply; 29+ messages in thread
From: Wolfgang Bumiller @ 2025-06-24 12:51 UTC (permalink / raw)
To: Filip Schauer; +Cc: pve-devel
I recently started reorganizing this repository so that bindings are in
the `bindings` submodule and require documentation.
See any of the `update <foo> module to new code convention` commits.
Essentially, the generated docs for the bindings submodule should be
the "perl documentation", and anything else should be rust specific
documentation.
On Wed, Jun 11, 2025 at 04:48:54PM +0200, Filip Schauer wrote:
> Signed-off-by: Filip Schauer <f.schauer@proxmox.com>
> ---
> pve-rs/Cargo.toml | 2 ++
> pve-rs/Makefile | 1 +
> pve-rs/debian/control | 2 ++
> pve-rs/src/lib.rs | 1 +
> pve-rs/src/oci.rs | 20 ++++++++++++++++++++
> 5 files changed, 26 insertions(+)
> create mode 100644 pve-rs/src/oci.rs
>
> diff --git a/pve-rs/Cargo.toml b/pve-rs/Cargo.toml
> index c7f11a3..00624c3 100644
> --- a/pve-rs/Cargo.toml
> +++ b/pve-rs/Cargo.toml
> @@ -20,6 +20,7 @@ hex = "0.4"
> http = "1"
> libc = "0.2"
> nix = "0.29"
> +oci-spec = "0.8.1"
> openssl = "0.10.40"
> serde = "1.0"
> serde_bytes = "0.11"
> @@ -37,6 +38,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 afe792a..d6a667f 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..243b428 100644
> --- a/pve-rs/debian/control
> +++ b/pve-rs/debian/control
> @@ -10,6 +10,7 @@ Build-Depends: cargo:native <!nocheck>,
> librust-http-1+default-dev (>= 0.2.7-~~),
> librust-libc-0.2+default-dev,
> librust-nix-0.29+default-dev,
> + librust-oci-spec-0.8+default-dev (>= 0.8.1-~~),
> librust-openssl-0.10+default-dev (>= 0.10.40-~~),
> librust-perlmod-0.14+default-dev (>= 0.13.5-~~),
> librust-perlmod-0.14+exporter-dev (>= 0.13.5-~~),
> @@ -24,6 +25,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/lib.rs b/pve-rs/src/lib.rs
> index bb979b9..1fe9e77 100644
> --- a/pve-rs/src/lib.rs
> +++ b/pve-rs/src/lib.rs
> @@ -12,6 +12,7 @@ use proxmox_notify::{Config, Notification, Severity};
> mod common;
>
> pub mod firewall;
> +pub mod oci;
> pub mod openid;
>
> pub mod bindings;
> diff --git a/pve-rs/src/oci.rs b/pve-rs/src/oci.rs
> new file mode 100644
> index 0000000..dd556aa
> --- /dev/null
> +++ b/pve-rs/src/oci.rs
> @@ -0,0 +1,20 @@
> +#[perlmod::package(name = "PVE::RS::OCI")]
> +mod export {
> + use anyhow::{Error, bail};
> + use oci_spec::image::Config;
I think `proxmox-oci` should re-export this type, then we only need to
depend on the one crate.
> + use proxmox_oci::{ParseError, ProxmoxOciError};
> +
> + #[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) => match err {
> + ProxmoxOciError::ParseError(ParseError::NotAnOciImage(_)) => Ok(None),
^ Why are we doing this?
> + err => bail!("{err}"),
> + },
> + }
> + }
> +}
> --
> 2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 29+ messages in thread
* Re: [pve-devel] [PATCH storage v2 10/11] allow .tar container templates
2025-06-11 14:49 ` [pve-devel] [PATCH storage v2 10/11] allow .tar container templates Filip Schauer
@ 2025-06-24 13:11 ` Wolfgang Bumiller
0 siblings, 0 replies; 29+ messages in thread
From: Wolfgang Bumiller @ 2025-06-24 13:11 UTC (permalink / raw)
To: Filip Schauer; +Cc: pve-devel, Thomas Lamprecht
*Mostly* LGTM and can be applied independently from the rest of the
series.
But I have 1 concern with a not so pretty suggestion... (see below)
On Wed, Jun 11, 2025 at 04:49:02PM +0200, Filip Schauer wrote:
> 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>
> ---
> 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 3962077..0d43df1 100755
> --- a/src/PVE/Storage.pm
> +++ b/src/PVE/Storage.pm
> @@ -117,7 +117,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(?:\.(gz|xz|zst|bz2))?/i;
This is a publicly exported regex. Potentially also used by external
plugins...
I am almost tempted to turn this into:
qr/\.(?|(tar)(?!\.)|tar(?:\.(gz|xz|zst|bz2))?)/i
`(?|...)` is the "branch reset" pattern, which means the first `(tar)`
and the second `(gz|xz...)` get the same capture group numbering.
The `(?!\.)` is a negative lookahead - since this RE is not anchored,
the `.tar` should only match if it is not followed by a dot.
This way, the hunk below would be unnecessary, since a simple `.tar`
file would end up putting "tar" into $2. Iow. the regex would be "API
compatibleš¤"
Not sure if it is worth the complexity of the regex, though...
>
> 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 1d8a8f9..76ffdb6 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 ? "t$2" : "tar" };
>
> } elsif ($tt eq 'backup') {
> next if $fn !~ m!/([^/]+$PVE::Storage::BACKUP_EXT_RE_2)$!;
> --
> 2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 29+ messages in thread
* Re: [pve-devel] [PATCH proxmox-perl-rs v2 02/11] add Perl mapping for OCI container image parser/extractor
2025-06-24 12:51 ` Wolfgang Bumiller
@ 2025-06-25 7:59 ` Filip Schauer
2025-06-25 8:10 ` Wolfgang Bumiller
0 siblings, 1 reply; 29+ messages in thread
From: Filip Schauer @ 2025-06-25 7:59 UTC (permalink / raw)
To: Wolfgang Bumiller; +Cc: pve-devel
On 24/06/2025 14:51, Wolfgang Bumiller wrote:
>> + #[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) => match err {
>> + ProxmoxOciError::ParseError(ParseError::NotAnOciImage(_)) => Ok(None),
> ^ Why are we doing this?
In the Perl code of the create_vm API method at PVE::API2::LXC, if a
.tar file is used as the container template,
PVE::RS::OCI::parse_and_extract_image is first attempted.
It returns undef, when the .tar file is not an OCI image. Thus we deduce
that the file is an LXC template. If some other kind of error occurrs,
(e.g. I/O error) then the Perl code should die.
The point is, there needs to be a way to differentiate between the file
actually being an LXC template, and some general failure.
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 29+ messages in thread
* Re: [pve-devel] [PATCH proxmox-perl-rs v2 02/11] add Perl mapping for OCI container image parser/extractor
2025-06-25 7:59 ` Filip Schauer
@ 2025-06-25 8:10 ` Wolfgang Bumiller
0 siblings, 0 replies; 29+ messages in thread
From: Wolfgang Bumiller @ 2025-06-25 8:10 UTC (permalink / raw)
To: Filip Schauer; +Cc: pve-devel
On Wed, Jun 25, 2025 at 09:59:08AM +0200, Filip Schauer wrote:
> On 24/06/2025 14:51, Wolfgang Bumiller wrote:
> > > + #[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) => match err {
> > > + ProxmoxOciError::ParseError(ParseError::NotAnOciImage(_)) => Ok(None),
> > ^ Why are we doing this?
>
> In the Perl code of the create_vm API method at PVE::API2::LXC, if a
> .tar file is used as the container template,
> PVE::RS::OCI::parse_and_extract_image is first attempted.
> It returns undef, when the .tar file is not an OCI image. Thus we deduce
> that the file is an LXC template. If some other kind of error occurrs,
> (e.g. I/O error) then the Perl code should die.
>
> The point is, there needs to be a way to differentiate between the file
> actually being an LXC template, and some general failure.
Given how the errors in the oci-spec work I think it would make more
sense to do this detection separately by simply looking for an
`oci-layout` file in the archive (or looking for all of `oci-layout`,
`blobs`, `index.json` at only the top level).
An an error in the OCI data should not cause us to try to extract this
as a regular container either.
In the long run, this needs to be a separate content type anyway, since
don't want to just extract tar files like this. Rather, we want to later
also add the ability to use the base image via overlayfs.
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 29+ messages in thread
* Re: [pve-devel] [PATCH proxmox v2 01/11] add proxmox-oci crate
2025-06-11 14:48 ` [pve-devel] [PATCH proxmox v2 01/11] add proxmox-oci crate Filip Schauer
2025-06-24 12:42 ` Wolfgang Bumiller
@ 2025-06-25 8:13 ` Wolfgang Bumiller
1 sibling, 0 replies; 29+ messages in thread
From: Wolfgang Bumiller @ 2025-06-25 8:13 UTC (permalink / raw)
To: Filip Schauer; +Cc: pve-devel
On Wed, Jun 11, 2025 at 04:48:53PM +0200, Filip Schauer wrote:
> This crate can parse and extract an OCI image bundled as a tar archive.
>
> Signed-off-by: Filip Schauer <f.schauer@proxmox.com>
> ---
> Cargo.toml | 1 +
> proxmox-oci/Cargo.toml | 22 ++++
> proxmox-oci/debian/changelog | 5 +
> proxmox-oci/debian/control | 47 ++++++++
> proxmox-oci/debian/debcargo.toml | 7 ++
> proxmox-oci/src/lib.rs | 196 +++++++++++++++++++++++++++++++
> proxmox-oci/src/oci_tar_image.rs | 167 ++++++++++++++++++++++++++
> 7 files changed, 445 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 bf9e83d7..8365b18a 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-rest-server",
> 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..3974cf48
> --- /dev/null
> +++ b/proxmox-oci/debian/control
> @@ -0,0 +1,47 @@
> +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.1.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.12+bindgen-dev <!nocheck>,
> + librust-zstd-0.12+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.1.0-~~),
> + librust-sha2-0.10+default-dev,
> + librust-tar-0.4+default-dev,
> + librust-thiserror-1+default-dev,
> + librust-zstd-0.12+bindgen-dev,
> + librust-zstd-0.12+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..cc5a1d46
> --- /dev/null
> +++ b/proxmox-oci/src/lib.rs
> @@ -0,0 +1,196 @@
> +use flate2::read::GzDecoder;
> +use oci_spec::image::{Arch, Config, ImageConfiguration, ImageManifest, MediaType};
> +use oci_spec::OciSpecError;
> +use oci_tar_image::OciTarImage;
> +use sha2::digest::generic_array::GenericArray;
> +use sha2::{Digest, Sha256};
> +use std::collections::HashMap;
> +use std::fs::File;
> +use std::io::{Read, Seek};
> +use std::path::PathBuf;
> +use std::str::FromStr;
> +use tar::Archive;
> +use thiserror::Error;
> +
> +pub mod oci_tar_image;
> +
> +fn compute_digest<R: Read, H: Digest>(
> + mut reader: R,
> + mut hasher: H,
> +) -> GenericArray<u8, H::OutputSize> {
> + let mut buf = proxmox_io::boxed::zeroed(4096);
> +
> + loop {
> + let bytes_read = reader.read(&mut buf).unwrap();
> + if bytes_read == 0 {
> + break hasher.finalize();
> + }
> +
> + hasher.update(&buf[..bytes_read]);
> + }
> +}
> +
> +fn compute_sha256<R: Read>(reader: R) -> oci_spec::image::Sha256Digest {
> + let digest = compute_digest(reader, Sha256::new());
> + oci_spec::image::Sha256Digest::from_str(&format!("{:x}", digest)).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>(
> + oci_tar_image: &mut OciTarImage<R>,
> + image_manifest: &ImageManifest,
> +) -> HashMap<oci_spec::image::Digest, oci_spec::image::Descriptor> {
> + let mut layer_mapping = HashMap::new();
> +
> + for layer in image_manifest.layers() {
> + let digest = match layer.media_type() {
> + MediaType::ImageLayer | MediaType::ImageLayerNonDistributable => {
> + Some(layer.digest().clone())
> + }
> + MediaType::ImageLayerGzip | MediaType::ImageLayerNonDistributableGzip => {
> + let compressed_blob = oci_tar_image.open_blob(layer.digest()).unwrap();
> + let decoder = GzDecoder::new(compressed_blob);
> + Some(compute_sha256(decoder).into())
> + }
> + MediaType::ImageLayerZstd | MediaType::ImageLayerNonDistributableZstd => {
> + let compressed_blob = oci_tar_image.open_blob(layer.digest()).unwrap();
> + let decoder = zstd::Decoder::new(compressed_blob).unwrap();
> + Some(compute_sha256(decoder).into())
> + }
> + _ => None,
> + };
> +
> + if let Some(digest) = digest {
> + layer_mapping.insert(digest, layer.clone());
> + }
> + }
> +
> + 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(
> + oci_tar_path: &str,
> + rootfs_path: &str,
> +) -> Result<Option<Config>, ProxmoxOciError> {
> + let (mut oci_tar_image, image_manifest, image_config) = parse_image(oci_tar_path)?;
> +
> + extract_image_rootfs(
> + &mut oci_tar_image,
> + &image_manifest,
> + &image_config,
> + rootfs_path.into(),
> + )?;
> +
> + Ok(image_config.config().clone())
> +}
> +
> +#[derive(Debug, Error)]
> +pub enum ParseError {
> + #[error("Not an OCI image: {0}")]
> + NotAnOciImage(OciSpecError),
> + #[error("Wrong media type")]
> + WrongMediaType,
> + #[error("IO error: {0}")]
> + IoError(#[from] std::io::Error),
> + #[error("Unsupported CPU architecture")]
> + UnsupportedArchitecture,
> + #[error("Missing image config")]
> + MissingImageConfig,
> +}
> +
> +impl From<OciSpecError> for ParseError {
> + fn from(oci_spec_err: OciSpecError) -> Self {
> + match oci_spec_err {
> + OciSpecError::Io(ioerr) => Self::IoError(ioerr),
> + ocierr => Self::NotAnOciImage(ocierr),
> + }
> + }
> +}
> +
> +fn parse_image(
> + oci_tar_path: &str,
> +) -> 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 image_config_file = oci_tar_image
> + .open_blob(image_config_descriptor.digest())
> + .ok_or(ParseError::MissingImageConfig)?;
> + let image_config = ImageConfiguration::from_reader(image_config_file)?;
> +
> + Ok((oci_tar_image, image_manifest, image_config))
> +}
> +
> +#[derive(Debug, Error)]
> +pub enum ExtractError {
> + #[error("Rootfs destination path not found")]
> + RootfsDestinationNotFound,
> + #[error("Unknown layer digest found in rootfs.diff_ids")]
> + UnknownLayerDigest,
> + #[error("Layer file mentioned in image manifest is missing")]
> + MissingLayerFile,
> + #[error("IO error: {0}")]
> + IoError(#[from] std::io::Error),
> + #[error("Layer has wrong media type")]
> + WrongMediaType,
> +}
> +
> +fn extract_image_rootfs<R: Read + Seek>(
> + oci_tar_image: &mut OciTarImage<R>,
> + image_manifest: &ImageManifest,
> + image_config: &ImageConfiguration,
> + target_path: PathBuf,
> +) -> Result<(), ExtractError> {
> + if !target_path.exists() {
> + return Err(ExtractError::RootfsDestinationNotFound);
> + }
> +
> + let 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::UnknownLayerDigest)?;
> + let layer_descriptor = layer_map
> + .get(&layer_digest)
> + .ok_or(ExtractError::UnknownLayerDigest)?;
> + let layer_file = oci_tar_image
> + .open_blob(layer_descriptor.digest())
> + .ok_or(ExtractError::MissingLayerFile)?;
> +
> + let tar_file: Box<dyn Read> = match layer_descriptor.media_type() {
> + MediaType::ImageLayer | MediaType::ImageLayerNonDistributable => Box::new(layer_file),
> + MediaType::ImageLayerGzip | MediaType::ImageLayerNonDistributableGzip => {
> + Box::new(GzDecoder::new(layer_file))
> + }
> + MediaType::ImageLayerZstd | MediaType::ImageLayerNonDistributableZstd => {
> + Box::new(zstd::Decoder::new(layer_file)?)
> + }
> + _ => return Err(ExtractError::WrongMediaType),
> + };
> +
> + let mut archive = Archive::new(tar_file);
> + archive.set_preserve_ownerships(true);
> + archive.unpack(&target_path)?;
After some more thought: this is actually insufficient.
We need to also handle whiteouts[1] (and later also extracting those
whiteouts as overlayfs-compatible whiteout device nodes and opaque dirs
via xattrs.
You can pull `joomla` as an example image with whiteouts. Eg.:
file: blobs/sha256/1017d0351d4278294b94bbede8960513af97265c3fff9a66a179f50df08b13fc
(...)
---------- 0/0 0 1970-01-01 01:00 etc/apache2/mods-enabled/.wh.mpm_event.conf
(...)
[1] https://github.com/opencontainers/image-spec/blob/main/layer.md#whiteouts
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 29+ messages in thread
* Re: [pve-devel] [PATCH container v2 06/11] configure static IP in LXC config for custom entrypoint
2025-06-11 14:48 ` [pve-devel] [PATCH container v2 06/11] configure static IP in LXC config for custom entrypoint Filip Schauer
@ 2025-06-25 8:26 ` Wolfgang Bumiller
2025-06-25 8:30 ` Wolfgang Bumiller
2025-07-09 12:45 ` Filip Schauer
0 siblings, 2 replies; 29+ messages in thread
From: Wolfgang Bumiller @ 2025-06-25 8:26 UTC (permalink / raw)
To: Filip Schauer; +Cc: pve-devel
On Wed, Jun 11, 2025 at 04:48:58PM +0200, Filip Schauer wrote:
> When a container uses the default `/sbin/init` entrypoint, network
I'm not a fan of this logic, also because it does not support 'dhcp' (in
which case it should IMO fail instead).
But maybe we can figure out an IP if SDN is in use to support the dhcp
case if there are static/permanent leases? (@Stefan?)
I think this should be handled with a separate key in the containers
network configuration. Maybe a "setup" property which defaults to
"container" and can be set to "host" (not sure if we ever need more,
if we know we don't, it could be a boolean...)
> 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>
> ---
> src/PVE/LXC.pm | 12 ++++++++++++
> 1 file changed, 12 insertions(+)
>
> diff --git a/src/PVE/LXC.pm b/src/PVE/LXC.pm
> index b2be27e..0131ac3 100644
> --- a/src/PVE/LXC.pm
> +++ b/src/PVE/LXC.pm
> @@ -818,6 +818,18 @@ 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
> + && PVE::LXC::Config->get_entrypoint($conf) ne "/sbin/init"
> + ) {
> + $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});
> + $raw .= "lxc.net.$ind.ipv6.address = $d->{ip6}\n"
> + if defined($d->{ip6}) && $d->{ip6} !~ /^(auto|dhcp|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;
> --
> 2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 29+ messages in thread
* Re: [pve-devel] [PATCH container v2 06/11] configure static IP in LXC config for custom entrypoint
2025-06-25 8:26 ` Wolfgang Bumiller
@ 2025-06-25 8:30 ` Wolfgang Bumiller
2025-06-25 8:52 ` Stefan Hanreich
2025-07-09 12:45 ` Filip Schauer
1 sibling, 1 reply; 29+ messages in thread
From: Wolfgang Bumiller @ 2025-06-25 8:30 UTC (permalink / raw)
To: Filip Schauer; +Cc: pve-devel
On Wed, Jun 25, 2025 at 10:26:20AM +0200, Wolfgang Bumiller wrote:
> On Wed, Jun 11, 2025 at 04:48:58PM +0200, Filip Schauer wrote:
> > When a container uses the default `/sbin/init` entrypoint, network
>
> I'm not a fan of this logic, also because it does not support 'dhcp' (in
> which case it should IMO fail instead).
(Missed that there will be a dhcp patch - that one could revert the
error message then - I'd still prefer to have an error initially here.
Anyway, the point about having a property to control this behavior still
stands.)
> But maybe we can figure out an IP if SDN is in use to support the dhcp
> case if there are static/permanent leases? (@Stefan?)
>
> I think this should be handled with a separate key in the containers
> network configuration. Maybe a "setup" property which defaults to
> "container" and can be set to "host" (not sure if we ever need more,
> if we know we don't, it could be a boolean...)
>
> > 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>
> > ---
> > src/PVE/LXC.pm | 12 ++++++++++++
> > 1 file changed, 12 insertions(+)
> >
> > diff --git a/src/PVE/LXC.pm b/src/PVE/LXC.pm
> > index b2be27e..0131ac3 100644
> > --- a/src/PVE/LXC.pm
> > +++ b/src/PVE/LXC.pm
> > @@ -818,6 +818,18 @@ 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
> > + && PVE::LXC::Config->get_entrypoint($conf) ne "/sbin/init"
> > + ) {
> > + $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});
> > + $raw .= "lxc.net.$ind.ipv6.address = $d->{ip6}\n"
> > + if defined($d->{ip6}) && $d->{ip6} !~ /^(auto|dhcp|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;
> > --
> > 2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 29+ messages in thread
* Re: [pve-devel] [PATCH container v2 09/11] manage DHCP for containers with custom entrypoint
2025-06-11 14:49 ` [pve-devel] [PATCH container v2 09/11] manage DHCP for containers with custom entrypoint Filip Schauer
@ 2025-06-25 8:50 ` Wolfgang Bumiller
2025-07-09 12:43 ` Filip Schauer
0 siblings, 1 reply; 29+ messages in thread
From: Wolfgang Bumiller @ 2025-06-25 8:50 UTC (permalink / raw)
To: Filip Schauer; +Cc: pve-devel
On Wed, Jun 11, 2025 at 04:49:01PM +0200, Filip Schauer wrote:
> 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>
> ---
> src/PVE/LXC.pm | 74 ++++++++++++++++++++++++++++++++++++++++---
> src/PVE/LXC/Config.pm | 6 +++-
> 2 files changed, 74 insertions(+), 6 deletions(-)
>
> diff --git a/src/PVE/LXC.pm b/src/PVE/LXC.pm
> index 0131ac3..e91b53a 100644
> --- a/src/PVE/LXC.pm
> +++ b/src/PVE/LXC.pm
> @@ -1004,6 +1004,8 @@ sub vm_stop_cleanup {
> PVE::Storage::deactivate_volumes($storage_cfg, $vollist);
> };
> warn $@ if $@; # avoid errors - just warn
> +
> + kill_dhclients($vmid, '*') if (PVE::LXC::Config->get_entrypoint($conf) ne "/sbin/init");
> }
>
> sub net_tap_plug : prototype($$) {
> @@ -1189,6 +1191,34 @@ 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',
^ This should probably get a fixme, I'd like to at some point enforce
that dhclient really only writes to `/var/lib/lxc/$vmid` and the 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;
> + kill 9, $pid if ($pid =~ m/^\d+$/);
^ To avoid any surprises we should properly untaint this instead:
next if $pid !~ /^(\d+)$/;
kill 9, $1;
> + unlink($pidfile);
> + }
> + }
> +}
> +
> sub update_ipconfig {
> my ($vmid, $conf, $opt, $eth, $newnet, $rootdir) = @_;
>
> @@ -1223,11 +1253,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 (PVE::LXC::Config->get_entrypoint($conf) ne "/sbin/init") {
> + 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;
> + }
> }
> }
>
> @@ -2707,6 +2747,30 @@ sub vm_start {
> }
> PVE::GuestHelpers::exec_hookscript($conf, $vmid, 'post-start');
>
> + 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";
^ When using this path over a potentially longer period of time it's
better to use
my ($pid, $pidfd) = PVE::LXC::open_lxc_pid($vmid);
The open pidfd should guard against pid reuse during these operations.
(In fact, any code using this path should keep the pid fd open. That's
something that can still be improved across the container code base in
the future.)
> +
> + if (PVE::LXC::Config->get_entrypoint($conf) ne "/sbin/init") {
^ This check could cover the entire hunk here.
> + 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('stop', $vmid, 6, $eth, $rootdir) };
^ Should be 'start'.
> + PVE::RESTEnvironment::log_warn("DHCP failed - $@") if $@;
> + }
> + }
> +
> return;
> }
>
> diff --git a/src/PVE/LXC/Config.pm b/src/PVE/LXC/Config.pm
> index d7d8b6a..854e711 100644
> --- a/src/PVE/LXC/Config.pm
> +++ b/src/PVE/LXC/Config.pm
> @@ -1490,9 +1490,13 @@ sub vmconfig_hotplug_pending {
> $cgroup->change_cpu_shares(undef);
> } elsif ($opt =~ m/^net(\d)$/) {
> my $netid = $1;
> + my $net = PVE::LXC::Config->parse_lxc_network($conf->{$opt});
> + if (PVE::LXC::Config->get_entrypoint($conf) ne "/sbin/init") {
> + PVE::LXC::kill_dhclients($vmid, $net->{name});
> + }
> +
> 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($net->{bridge}, $net->{hwaddr}, $conf->{hostname}) };
> warn $@ if $@;
> --
> 2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 29+ messages in thread
* Re: [pve-devel] [PATCH container v2 06/11] configure static IP in LXC config for custom entrypoint
2025-06-25 8:30 ` Wolfgang Bumiller
@ 2025-06-25 8:52 ` Stefan Hanreich
0 siblings, 0 replies; 29+ messages in thread
From: Stefan Hanreich @ 2025-06-25 8:52 UTC (permalink / raw)
To: Wolfgang Bumiller, Filip Schauer; +Cc: pve-devel
On 6/25/25 10:30, Wolfgang Bumiller wrote:
> On Wed, Jun 25, 2025 at 10:26:20AM +0200, Wolfgang Bumiller wrote:
>> On Wed, Jun 11, 2025 at 04:48:58PM +0200, Filip Schauer wrote:
>>> When a container uses the default `/sbin/init` entrypoint, network
>>
>> I'm not a fan of this logic, also because it does not support 'dhcp' (in
>> which case it should IMO fail instead).
>
> (Missed that there will be a dhcp patch - that one could revert the
> error message then - I'd still prefer to have an error initially here.
> Anyway, the point about having a property to control this behavior still
> stands.)
I think that resolves the point w.r.t SDN then as well?
>> But maybe we can figure out an IP if SDN is in use to support the dhcp
>> case if there are static/permanent leases? (@Stefan?)
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 29+ messages in thread
* [pve-devel] applied: [PATCH container v2 03/11] config: whitelist lxc.init.cwd
2025-06-11 14:48 ` [pve-devel] [PATCH container v2 03/11] config: whitelist lxc.init.cwd Filip Schauer
@ 2025-06-25 9:00 ` Wolfgang Bumiller
0 siblings, 0 replies; 29+ messages in thread
From: Wolfgang Bumiller @ 2025-06-25 9:00 UTC (permalink / raw)
To: Filip Schauer; +Cc: pve-devel
applied this one, thanks
On Wed, Jun 11, 2025 at 04:48:55PM +0200, Filip Schauer wrote:
> This parameter allows setting the working directory of the init process
> in the container. This can be used by containers created from an OCI
> image, that specifies a custom working directory.
>
> Signed-off-by: Filip Schauer <f.schauer@proxmox.com>
> ---
> src/PVE/LXC/Config.pm | 1 +
> 1 file changed, 1 insertion(+)
>
> diff --git a/src/PVE/LXC/Config.pm b/src/PVE/LXC/Config.pm
> index 0740e8c..49067ea 100644
> --- a/src/PVE/LXC/Config.pm
> +++ b/src/PVE/LXC/Config.pm
> @@ -641,6 +641,7 @@ my $valid_lxc_conf_keys = {
> 'lxc.signal.reboot' => 1,
> 'lxc.signal.stop' => 1,
> 'lxc.init.cmd' => 1,
> + 'lxc.init.cwd' => 1,
> 'lxc.pty.max' => 1,
> 'lxc.console.logfile' => 1,
> 'lxc.console.path' => 1,
> --
> 2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 29+ messages in thread
* Re: [pve-devel] superseded: [PATCH container/manager/proxmox{, -perl-rs}/storage v2 00/11] support OCI images as container templates
2025-06-11 14:48 [pve-devel] [PATCH container/manager/proxmox{, -perl-rs}/storage v2 00/11] support OCI images as container templates Filip Schauer
` (11 preceding siblings ...)
2025-06-17 8:01 ` [pve-devel] [PATCH container/manager/proxmox{, -perl-rs}/storage v2 00/11] support OCI images as container templates Christoph Heiss
@ 2025-07-09 12:40 ` Filip Schauer
12 siblings, 0 replies; 29+ messages in thread
From: Filip Schauer @ 2025-07-09 12:40 UTC (permalink / raw)
To: pve-devel
Superseded by:
https://lore.proxmox.com/pve-devel/20250709123435.64796-1-f.schauer@proxmox.com
On 11/06/2025 16:48, Filip Schauer wrote:
> Add basic support for OCI (Open Container Initiative) images [0] as
> container templates.
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 29+ messages in thread
* Re: [pve-devel] [PATCH container v2 09/11] manage DHCP for containers with custom entrypoint
2025-06-25 8:50 ` Wolfgang Bumiller
@ 2025-07-09 12:43 ` Filip Schauer
2025-07-09 13:00 ` Wolfgang Bumiller
0 siblings, 1 reply; 29+ messages in thread
From: Filip Schauer @ 2025-07-09 12:43 UTC (permalink / raw)
To: Wolfgang Bumiller; +Cc: pve-devel
On 25/06/2025 10:50, Wolfgang Bumiller wrote:
>> + my $pid = PVE::LXC::find_lxc_pid($vmid);
>> + my $rootdir = "/proc/$pid/root";
> ^ When using this path over a potentially longer period of time it's
> better to use
>
> my ($pid, $pidfd) = PVE::LXC::open_lxc_pid($vmid);
>
> The open pidfd should guard against pid reuse during these operations.
Unfortunatelly /usr/share/lxc/hooks/dhclient-script expects a path in
$ROOTFS.
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 29+ messages in thread
* Re: [pve-devel] [PATCH container v2 06/11] configure static IP in LXC config for custom entrypoint
2025-06-25 8:26 ` Wolfgang Bumiller
2025-06-25 8:30 ` Wolfgang Bumiller
@ 2025-07-09 12:45 ` Filip Schauer
1 sibling, 0 replies; 29+ messages in thread
From: Filip Schauer @ 2025-07-09 12:45 UTC (permalink / raw)
To: Wolfgang Bumiller; +Cc: pve-devel
On 25/06/2025 10:26, Wolfgang Bumiller wrote:
> I think this should be handled with a separate key in the containers
> network configuration. Maybe a "setup" property which defaults to
> "container" and can be set to "host" (not sure if we ever need more,
> if we know we don't, it could be a boolean...)
I agree that checking the entrypoint directly in multiple places, to
determine whether IP configuration should be managed by the host, is not
ideal. But setting a property for every net[n] individually would also
require us to set that property every time a network interface is added.
So instead in v3 I added an "ipmanagehost" boolean directly to pct.conf.
If we want we could still add a property to the network configuration
that overrides this behaviour.
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 29+ messages in thread
* Re: [pve-devel] [PATCH container/manager/proxmox{, -perl-rs}/storage v2 00/11] support OCI images as container templates
2025-06-17 8:01 ` [pve-devel] [PATCH container/manager/proxmox{, -perl-rs}/storage v2 00/11] support OCI images as container templates Christoph Heiss
@ 2025-07-09 12:50 ` Filip Schauer
0 siblings, 0 replies; 29+ messages in thread
From: Filip Schauer @ 2025-07-09 12:50 UTC (permalink / raw)
To: Christoph Heiss; +Cc: Proxmox VE development discussion
On 17/06/2025 10:01, Christoph Heiss wrote:
> I also test with `ghcr.io/nixos/nix:latest`, which interestingly fails
> to start with
>
> DEBUG utils - ../src/lxc/utils.c:run_buffer:560 - Script exec /usr/share/lxcfs/lxc.mount.hook 107 lxc mount produced output: /usr/share/lxcfs/lxc.mount.hook: 15: readlink: Permission denied
>
> Not sure what is going on there, but I don't think it's directly related
> to this series, rather just some OCI/Nix weirdness.
`ghcr.io/nixos/nix:latest` sets a custom $PATH, leading to
`/usr/share/lxcfs/lxc.mount.hook` not finding the `readlink` binary.
This is fixed in v3 10/13.
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 29+ messages in thread
* Re: [pve-devel] [PATCH container v2 09/11] manage DHCP for containers with custom entrypoint
2025-07-09 12:43 ` Filip Schauer
@ 2025-07-09 13:00 ` Wolfgang Bumiller
0 siblings, 0 replies; 29+ messages in thread
From: Wolfgang Bumiller @ 2025-07-09 13:00 UTC (permalink / raw)
To: Filip Schauer; +Cc: pve-devel
On Wed, Jul 09, 2025 at 02:43:06PM +0200, Filip Schauer wrote:
> On 25/06/2025 10:50, Wolfgang Bumiller wrote:
> > > + my $pid = PVE::LXC::find_lxc_pid($vmid);
> > > + my $rootdir = "/proc/$pid/root";
> > ^ When using this path over a potentially longer period of time it's
> > better to use
> >
> > my ($pid, $pidfd) = PVE::LXC::open_lxc_pid($vmid);
> >
> > The open pidfd should guard against pid reuse during these operations.
>
> Unfortunatelly /usr/share/lxc/hooks/dhclient-script expects a path in
> $ROOTFS.
Yeah.
So replace the `my $pid = ...` line with what I wrote to keep the
`$pidfd` around for as long as `$rootdir` is being used.
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 29+ messages in thread
end of thread, other threads:[~2025-07-09 12:59 UTC | newest]
Thread overview: 29+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2025-06-11 14:48 [pve-devel] [PATCH container/manager/proxmox{, -perl-rs}/storage v2 00/11] support OCI images as container templates Filip Schauer
2025-06-11 14:48 ` [pve-devel] [PATCH proxmox v2 01/11] add proxmox-oci crate Filip Schauer
2025-06-24 12:42 ` Wolfgang Bumiller
2025-06-25 8:13 ` Wolfgang Bumiller
2025-06-11 14:48 ` [pve-devel] [PATCH proxmox-perl-rs v2 02/11] add Perl mapping for OCI container image parser/extractor Filip Schauer
2025-06-24 12:51 ` Wolfgang Bumiller
2025-06-25 7:59 ` Filip Schauer
2025-06-25 8:10 ` Wolfgang Bumiller
2025-06-11 14:48 ` [pve-devel] [PATCH container v2 03/11] config: whitelist lxc.init.cwd Filip Schauer
2025-06-25 9:00 ` [pve-devel] applied: " Wolfgang Bumiller
2025-06-11 14:48 ` [pve-devel] [PATCH container v2 04/11] add support for OCI images as container templates Filip Schauer
2025-06-11 14:48 ` [pve-devel] [PATCH container v2 05/11] config: add entrypoint parameter Filip Schauer
2025-06-11 14:48 ` [pve-devel] [PATCH container v2 06/11] configure static IP in LXC config for custom entrypoint Filip Schauer
2025-06-25 8:26 ` Wolfgang Bumiller
2025-06-25 8:30 ` Wolfgang Bumiller
2025-06-25 8:52 ` Stefan Hanreich
2025-07-09 12:45 ` Filip Schauer
2025-06-11 14:48 ` [pve-devel] [PATCH container v2 07/11] setup: debian: create /etc/network path if missing Filip Schauer
2025-06-11 14:49 ` [pve-devel] [PATCH container v2 08/11] setup: recursively mkdir /etc/systemd/{network, system-preset} Filip Schauer
2025-06-11 14:49 ` [pve-devel] [PATCH container v2 09/11] manage DHCP for containers with custom entrypoint Filip Schauer
2025-06-25 8:50 ` Wolfgang Bumiller
2025-07-09 12:43 ` Filip Schauer
2025-07-09 13:00 ` Wolfgang Bumiller
2025-06-11 14:49 ` [pve-devel] [PATCH storage v2 10/11] allow .tar container templates Filip Schauer
2025-06-24 13:11 ` Wolfgang Bumiller
2025-06-11 14:49 ` [pve-devel] [PATCH manager v2 11/11] ui: storage upload: accept *.tar files as vztmpl Filip Schauer
2025-06-17 8:01 ` [pve-devel] [PATCH container/manager/proxmox{, -perl-rs}/storage v2 00/11] support OCI images as container templates Christoph Heiss
2025-07-09 12:50 ` Filip Schauer
2025-07-09 12:40 ` [pve-devel] superseded: " Filip Schauer
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal