public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [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
                   ` (11 more replies)
  0 siblings, 12 replies; 13+ 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] 13+ 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-11 14:48 ` [pve-devel] [PATCH proxmox-perl-rs v2 02/11] add Perl mapping for OCI container image parser/extractor Filip Schauer
                   ` (10 subsequent siblings)
  11 siblings, 0 replies; 13+ 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] 13+ 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-11 14:48 ` [pve-devel] [PATCH container v2 03/11] config: whitelist lxc.init.cwd Filip Schauer
                   ` (9 subsequent siblings)
  11 siblings, 0 replies; 13+ 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] 13+ 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-11 14:48 ` [pve-devel] [PATCH container v2 04/11] add support for OCI images as container templates Filip Schauer
                   ` (8 subsequent siblings)
  11 siblings, 0 replies; 13+ 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] 13+ 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
                   ` (7 subsequent siblings)
  11 siblings, 0 replies; 13+ 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] 13+ 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
                   ` (6 subsequent siblings)
  11 siblings, 0 replies; 13+ 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] 13+ 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-11 14:48 ` [pve-devel] [PATCH container v2 07/11] setup: debian: create /etc/network path if missing Filip Schauer
                   ` (5 subsequent siblings)
  11 siblings, 0 replies; 13+ 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] 13+ 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
                   ` (4 subsequent siblings)
  11 siblings, 0 replies; 13+ 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] 13+ 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
                   ` (3 subsequent siblings)
  11 siblings, 0 replies; 13+ 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] 13+ 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-11 14:49 ` [pve-devel] [PATCH storage v2 10/11] allow .tar container templates Filip Schauer
                   ` (2 subsequent siblings)
  11 siblings, 0 replies; 13+ 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] 13+ 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-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
  11 siblings, 0 replies; 13+ 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] 13+ 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
  11 siblings, 0 replies; 13+ 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] 13+ 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
  11 siblings, 0 replies; 13+ 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] 13+ messages in thread

end of thread, other threads:[~2025-06-17  8:02 UTC | newest]

Thread overview: 13+ 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-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 ` [pve-devel] [PATCH container v2 03/11] config: whitelist lxc.init.cwd Filip Schauer
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-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-11 14:49 ` [pve-devel] [PATCH storage v2 10/11] allow .tar container templates Filip Schauer
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

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