public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [pve-devel] [RFC PATCH installer 0/5] fix #5579: allow specifying optional first-boot script
@ 2024-11-13 13:59 Christoph Heiss
  2024-11-13 13:59 ` [pve-devel] [RFC PATCH installer 1/5] common: add function for issuing HTTP GET requests Christoph Heiss
                   ` (4 more replies)
  0 siblings, 5 replies; 6+ messages in thread
From: Christoph Heiss @ 2024-11-13 13:59 UTC (permalink / raw)
  To: pve-devel

This implements #5579 [0] as proposed by Thomas [1].

Sending it as an RFC as probably some bike-shedding has still to be done
anyway, in regards of naming/filesystem locations etc. -- but should
give an good idea how it can/should work in the end.

Adds a new (optional) section to the auto-installer answer file called
`first-boot`, which can be used to the configure a script/executable
file to run on the first boot after the installation.

To used the baked-in (via the `proxmox-auto-install-assistant prepare-iso
--on-first-boot`) file from the ISO:

  [first-boot]
  source = "from-iso"

Or fetching it from a URL:

  [first-boot]
  source = "from-url"
  url = "http://example.com/first-boot"
  cert_fingerprint = ".." # if needed

The structure the section is mostly taken from the `network` section to
provide consistency.

[0] https://bugzilla.proxmox.com/show_bug.cgi?id=5579
[1] https://bugzilla.proxmox.com/show_bug.cgi?id=5579#c5

Inner workings
==============

This creates a new package `proxmox-first-boot`, which only has a
systemd .service file. The service is conditioned to start based on the 
existence of a flag file in /var/lib/proxmox-first-boot, which will
be created by the installer if such a script is supplied by the user.

Q: The condition could maybe be set directly on the script instead of a
   separate file, since the executable file is only present when
   supplied anyway?

Testing
=======

Tested this with the latest (8.2-2) PVE ISO, both baking it into the ISO
and fetching it from an URL. Did not test with PBS explicitly (yet), but
I see no reason why it shouldn't work, as it is completely
product-agnostic.

Diffstat
========

Christoph Heiss (5):
  common: add function for issuing HTTP GET requests
  fix #5579: first-boot: add initial service packaging
  fix #5579: auto-install-assistant: enable baking in first-boot script
  fix #5579: auto-installer: add optional first-boot hook script
  fix #5579: install: copy over `proxmox-first-boot` script if present

 Makefile                                      | 13 +++-
 Proxmox/Install.pm                            | 20 ++++++
 debian/control                                |  7 ++
 debian/proxmox-first-boot.install             |  1 +
 debian/rules.proxmox-first-boot               | 13 ++++
 proxmox-auto-install-assistant/Cargo.toml     |  1 +
 proxmox-auto-install-assistant/src/main.rs    | 17 +++++
 proxmox-auto-installer/Cargo.toml             |  2 +-
 proxmox-auto-installer/src/answer.rs          | 27 +++++++
 .../src/bin/proxmox-auto-installer.rs         | 42 +++++++++--
 proxmox-auto-installer/src/utils.rs           | 15 +++-
 proxmox-first-boot/Makefile                   | 11 +++
 .../etc/proxmox-first-boot.service            | 16 +++++
 proxmox-installer-common/src/http.rs          | 71 +++++++++++++------
 proxmox-installer-common/src/lib.rs           |  3 +
 15 files changed, 227 insertions(+), 32 deletions(-)
 create mode 100644 debian/proxmox-first-boot.install
 create mode 100644 debian/rules.proxmox-first-boot
 create mode 100644 proxmox-first-boot/Makefile
 create mode 100644 proxmox-first-boot/etc/proxmox-first-boot.service

-- 
2.47.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] 6+ messages in thread

* [pve-devel] [RFC PATCH installer 1/5] common: add function for issuing HTTP GET requests
  2024-11-13 13:59 [pve-devel] [RFC PATCH installer 0/5] fix #5579: allow specifying optional first-boot script Christoph Heiss
@ 2024-11-13 13:59 ` Christoph Heiss
  2024-11-13 13:59 ` [pve-devel] [RFC PATCH installer 2/5] fix #5579: first-boot: add initial service packaging Christoph Heiss
                   ` (3 subsequent siblings)
  4 siblings, 0 replies; 6+ messages in thread
From: Christoph Heiss @ 2024-11-13 13:59 UTC (permalink / raw)
  To: pve-devel

Factors out the user-agent building into a separate function and then
re-uses that for get().

This has the side-effect that now for all requests issued by post() a
timeout of 60s is applied. Previously, this was only done when an
explicit fingerprint was given. Minute change and shouldn't effect
anything.

Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
 proxmox-installer-common/src/http.rs | 71 +++++++++++++++++++---------
 1 file changed, 48 insertions(+), 23 deletions(-)

diff --git a/proxmox-installer-common/src/http.rs b/proxmox-installer-common/src/http.rs
index b754ed8..f4afe14 100644
--- a/proxmox-installer-common/src/http.rs
+++ b/proxmox-installer-common/src/http.rs
@@ -4,33 +4,25 @@ use sha2::{Digest, Sha256};
 use std::sync::Arc;
 use ureq::{Agent, AgentBuilder};
 
-/// Issues a POST request with the payload (JSON). Optionally a SHA256 fingerprint can be used to
-/// check the cert against it, instead of the regular cert validation.
+/// Builds an [`Agent`] with TLS suitable set up, depending whether a custom fingerprint was
+/// supplied or not. If a fingerprint was supplied, only matching certificates will be accepted.
+/// Otherwise, the system certificate store is loaded.
+///
 /// To gather the sha256 fingerprint you can use the following command:
 /// ```no_compile
 /// openssl s_client -connect <host>:443 < /dev/null 2>/dev/null | openssl x509 -fingerprint -sha256  -noout -in /dev/stdin
 /// ```
 ///
 /// # Arguments
-/// * `url` - URL to call
 /// * `fingerprint` - SHA256 cert fingerprint if certificate pinning should be used. Optional.
-/// * `payload` - The payload to send to the server. Expected to be a JSON formatted string.
-pub fn post(url: &str, fingerprint: Option<&str>, payload: String) -> Result<String> {
-    let answer;
-
+fn build_agent(fingerprint: Option<&str>) -> Result<Agent> {
     if let Some(fingerprint) = fingerprint {
         let tls_config = ClientConfig::builder()
             .with_safe_defaults()
             .with_custom_certificate_verifier(VerifyCertFingerprint::new(fingerprint)?)
             .with_no_client_auth();
 
-        let agent: Agent = AgentBuilder::new().tls_config(Arc::new(tls_config)).build();
-
-        answer = agent
-            .post(url)
-            .set("Content-Type", "application/json; charset=utf-8")
-            .send_string(&payload)?
-            .into_string()?;
+        Ok(AgentBuilder::new().tls_config(Arc::new(tls_config)).build())
     } else {
         let mut roots = rustls::RootCertStore::empty();
         for cert in rustls_native_certs::load_native_certs()? {
@@ -42,18 +34,51 @@ pub fn post(url: &str, fingerprint: Option<&str>, payload: String) -> Result<Str
             .with_root_certificates(roots)
             .with_no_client_auth();
 
-        let agent = AgentBuilder::new()
+        Ok(AgentBuilder::new()
             .tls_connector(Arc::new(native_tls::TlsConnector::new()?))
             .tls_config(Arc::new(tls_config))
-            .build();
-        answer = agent
-            .post(url)
-            .set("Content-Type", "application/json; charset=utf-8")
-            .timeout(std::time::Duration::from_secs(60))
-            .send_string(&payload)?
-            .into_string()?;
+            .build())
     }
-    Ok(answer)
+}
+
+/// Issues a GET request to the specified URL and fetches the response. Optionally a SHA256
+/// fingerprint can be used to check the certificate against it, instead of the regular certificate
+/// validation.
+///
+/// To gather the sha256 fingerprint you can use the following command:
+/// ```no_compile
+/// openssl s_client -connect <host>:443 < /dev/null 2>/dev/null | openssl x509 -fingerprint -sha256  -noout -in /dev/stdin
+/// ```
+///
+/// # Arguments
+/// * `url` - URL to fetch
+/// * `fingerprint` - SHA256 cert fingerprint if certificate pinning should be used. Optional.
+pub fn get(url: &str, fingerprint: Option<&str>) -> Result<String> {
+    Ok(build_agent(fingerprint)?
+        .get(url)
+        .timeout(std::time::Duration::from_secs(60))
+        .call()?
+        .into_string()?)
+}
+
+/// Issues a POST request with the payload (JSON). Optionally a SHA256 fingerprint can be used to
+/// check the cert against it, instead of the regular cert validation.
+/// To gather the sha256 fingerprint you can use the following command:
+/// ```no_compile
+/// openssl s_client -connect <host>:443 < /dev/null 2>/dev/null | openssl x509 -fingerprint -sha256  -noout -in /dev/stdin
+/// ```
+///
+/// # Arguments
+/// * `url` - URL to call
+/// * `fingerprint` - SHA256 cert fingerprint if certificate pinning should be used. Optional.
+/// * `payload` - The payload to send to the server. Expected to be a JSON formatted string.
+pub fn post(url: &str, fingerprint: Option<&str>, payload: String) -> Result<String> {
+    Ok(build_agent(fingerprint)?
+        .post(url)
+        .set("Content-Type", "application/json; charset=utf-8")
+        .timeout(std::time::Duration::from_secs(60))
+        .send_string(&payload)?
+        .into_string()?)
 }
 
 struct VerifyCertFingerprint {
-- 
2.47.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] 6+ messages in thread

* [pve-devel] [RFC PATCH installer 2/5] fix #5579: first-boot: add initial service packaging
  2024-11-13 13:59 [pve-devel] [RFC PATCH installer 0/5] fix #5579: allow specifying optional first-boot script Christoph Heiss
  2024-11-13 13:59 ` [pve-devel] [RFC PATCH installer 1/5] common: add function for issuing HTTP GET requests Christoph Heiss
@ 2024-11-13 13:59 ` Christoph Heiss
  2024-11-13 13:59 ` [pve-devel] [RFC PATCH installer 3/5] fix #5579: auto-install-assistant: enable baking in first-boot script Christoph Heiss
                   ` (2 subsequent siblings)
  4 siblings, 0 replies; 6+ messages in thread
From: Christoph Heiss @ 2024-11-13 13:59 UTC (permalink / raw)
  To: pve-devel

While there is the `systemd-first-boot.service`, it uses the
non-existence of `/etc/machine-id` as condition to run. As we already
set up that file in the installer ourselves, we cannot use that.

Instead our service depends on a custom flag file in
/var/lib/proxmox-first-boot and will only run if that is present.

Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
 Makefile                                         | 13 ++++++++++---
 debian/control                                   |  7 +++++++
 debian/proxmox-first-boot.install                |  1 +
 debian/rules.proxmox-first-boot                  | 13 +++++++++++++
 proxmox-first-boot/Makefile                      | 11 +++++++++++
 .../etc/proxmox-first-boot.service               | 16 ++++++++++++++++
 6 files changed, 58 insertions(+), 3 deletions(-)
 create mode 100644 debian/proxmox-first-boot.install
 create mode 100644 debian/rules.proxmox-first-boot
 create mode 100644 proxmox-first-boot/Makefile
 create mode 100644 proxmox-first-boot/etc/proxmox-first-boot.service

diff --git a/Makefile b/Makefile
index d85347f..a17f6c5 100644
--- a/Makefile
+++ b/Makefile
@@ -5,6 +5,10 @@ BUILDDIR ?= $(PACKAGE)-$(DEB_VERSION_UPSTREAM)
 
 DEB=$(PACKAGE)_$(DEB_VERSION)_$(DEB_HOST_ARCH).deb
 ASSISTANT_DEB=proxmox-auto-install-assistant_$(DEB_VERSION)_$(DEB_HOST_ARCH).deb
+FIRST_BOOT_DEB=proxmox-first-boot_$(DEB_VERSION)_$(DEB_HOST_ARCH).deb
+
+ALL_DEBS = $(DEB) $(ASSISTANT_DEB) $(FIRST_BOOT_DEB)
+
 DSC=$(PACKAGE)_$(DEB_VERSION).dsc
 
 CARGO ?= cargo
@@ -61,6 +65,7 @@ $(BUILDDIR):
 	  proxmox-tui-installer/ \
 	  proxmox-installer-common/ \
 	  proxmox-post-hook \
+	  proxmox-first-boot \
 	  test/ \
 	  $(SHELL_SCRIPTS) \
 	  $@.tmp
@@ -73,9 +78,10 @@ country.dat: country.pl
 
 deb: $(DEB)
 $(ASSISTANT_DEB): $(DEB)
+$(FIRST_BOOT_DEB): $(DEB)
 $(DEB): $(BUILDDIR)
 	cd $(BUILDDIR); dpkg-buildpackage -b -us -uc
-	lintian $(DEB) $(ASSISTANT_DEB)
+	lintian $(ALL_DEBS)
 
 test-$(DEB): $(INSTALLER_SOURCES)
 	rsync --exclude='test*.img' --exclude='*.deb' --exclude='build' -a * build
@@ -114,6 +120,7 @@ HTMLDIR=$(VARLIBDIR)/html/common
 install: $(INSTALLER_SOURCES) $(COMPILED_BINS)
 	$(MAKE) -C banner install
 	$(MAKE) -C Proxmox install
+	$(MAKE) -C proxmox-first-boot install
 	install -D -m 644 interfaces $(DESTDIR)/etc/network/interfaces
 	install -D -m 755 fake-start-stop-daemon $(VARLIBDIR)/fake-start-stop-daemon
 	install -D -m 755 policy-disable-rc.d $(VARLIBDIR)/policy-disable-rc.d
@@ -143,8 +150,8 @@ cargo-build:
 
 .PHONY: upload
 upload: UPLOAD_DIST ?= $(DEB_DISTRIBUTION)
-upload: $(DEB) $(ASSISTANT_DEB)
-	tar cf - $(DEB) $(ASSISTANT_DEB) | ssh -X repoman@repo.proxmox.com -- upload --product pve,pmg,pbs --dist $(UPLOAD_DIST)
+upload: $(ALL_DEBS)
+	tar cf - $(ALL_DEBS) | ssh -X repoman@repo.proxmox.com -- upload --product pve,pmg,pbs --dist $(UPLOAD_DIST)
 
 %.img:
 	truncate -s 2G $@
diff --git a/debian/control b/debian/control
index ff00cc2..fd0f4df 100644
--- a/debian/control
+++ b/debian/control
@@ -62,3 +62,10 @@ Description: Assistant to help with automated installations
  Provides a helper that can assist with creating an answer file for a automated
  installation of a Proxmox project, and preparing a official ISO image to use
  this answer file.
+
+Package: proxmox-first-boot
+Architecture: any
+Depends: ${misc:Depends},
+Description: Service which runs on the first system boot for additional setup
+ Provides a service which will run on the first boot if the a script was
+ configured through the auto-installer.
diff --git a/debian/proxmox-first-boot.install b/debian/proxmox-first-boot.install
new file mode 100644
index 0000000..52f25d2
--- /dev/null
+++ b/debian/proxmox-first-boot.install
@@ -0,0 +1 @@
+lib/systemd/system/proxmox-first-boot.service
diff --git a/debian/rules.proxmox-first-boot b/debian/rules.proxmox-first-boot
new file mode 100644
index 0000000..5b30d35
--- /dev/null
+++ b/debian/rules.proxmox-first-boot
@@ -0,0 +1,13 @@
+#!/usr/bin/make -f
+# See debhelper(7) (uncomment to enable)
+# output every command that modifies files on the build system.
+#DH_VERBOSE = 1
+
+%:
+	dh $@
+
+override_dh_missing:
+	dh_missing --fail-missing
+
+override_dh_installsystemd:
+	dh_installsystemd --no-stop-on-upgrade --no-start proxmox-first-boot.service
diff --git a/proxmox-first-boot/Makefile b/proxmox-first-boot/Makefile
new file mode 100644
index 0000000..137de23
--- /dev/null
+++ b/proxmox-first-boot/Makefile
@@ -0,0 +1,11 @@
+all:
+
+DESTDIR =
+LIBSYSTEMD_DIR = $(DESTDIR)/lib/systemd/system
+
+.PHONY: install
+install: etc/proxmox-first-boot.service
+	install -D -m 644 etc/proxmox-first-boot.service $(LIBSYSTEMD_DIR)/proxmox-first-boot.service
+
+.PHONY: clean
+clean:
diff --git a/proxmox-first-boot/etc/proxmox-first-boot.service b/proxmox-first-boot/etc/proxmox-first-boot.service
new file mode 100644
index 0000000..046bb24
--- /dev/null
+++ b/proxmox-first-boot/etc/proxmox-first-boot.service
@@ -0,0 +1,16 @@
+[Unit]
+Description=Proxmox First Boot Setup
+After=systemd-remount-fs.service
+Before=network-pre.target
+Wants=network-pre.target
+ConditionPathExists=/var/lib/proxmox-first-boot/pending-first-boot-setup
+ConditionPathIsReadWrite=/var/lib
+
+[Service]
+Type=oneshot
+RemainAfterExit=yes
+ExecStart=/var/lib/proxmox-first-boot/proxmox-first-boot
+ExecStartPost=/usr/bin/rm -v /var/lib/proxmox-first-boot/pending-first-boot-setup
+
+[Install]
+WantedBy=network.target
-- 
2.47.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] 6+ messages in thread

* [pve-devel] [RFC PATCH installer 3/5] fix #5579: auto-install-assistant: enable baking in first-boot script
  2024-11-13 13:59 [pve-devel] [RFC PATCH installer 0/5] fix #5579: allow specifying optional first-boot script Christoph Heiss
  2024-11-13 13:59 ` [pve-devel] [RFC PATCH installer 1/5] common: add function for issuing HTTP GET requests Christoph Heiss
  2024-11-13 13:59 ` [pve-devel] [RFC PATCH installer 2/5] fix #5579: first-boot: add initial service packaging Christoph Heiss
@ 2024-11-13 13:59 ` Christoph Heiss
  2024-11-13 13:59 ` [pve-devel] [RFC PATCH installer 4/5] fix #5579: auto-installer: add optional first-boot hook script Christoph Heiss
  2024-11-13 13:59 ` [pve-devel] [RFC PATCH installer 5/5] fix #5579: install: copy over `proxmox-first-boot` script if present Christoph Heiss
  4 siblings, 0 replies; 6+ messages in thread
From: Christoph Heiss @ 2024-11-13 13:59 UTC (permalink / raw)
  To: pve-devel

Adds a new parameter `--on-first-boot` to the `prepare-iso` command, to
specify a file to bake into the ISO.

To later use it with the auto-installer, the following must be set in
the answer file:

  [first-boot]
  source = "from-iso"

Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
 proxmox-auto-install-assistant/Cargo.toml  |  1 +
 proxmox-auto-install-assistant/src/main.rs | 17 +++++++++++++++++
 proxmox-installer-common/src/lib.rs        |  3 +++
 3 files changed, 21 insertions(+)

diff --git a/proxmox-auto-install-assistant/Cargo.toml b/proxmox-auto-install-assistant/Cargo.toml
index c4486f8..07e6ffb 100644
--- a/proxmox-auto-install-assistant/Cargo.toml
+++ b/proxmox-auto-install-assistant/Cargo.toml
@@ -13,6 +13,7 @@ homepage = "https://www.proxmox.com"
 [dependencies]
 anyhow.workspace = true
 log.workspace = true
+proxmox-installer-common.workspace = true
 proxmox-auto-installer.workspace = true
 serde = { workspace = true, features = ["derive"] }
 serde_json.workspace = true
diff --git a/proxmox-auto-install-assistant/src/main.rs b/proxmox-auto-install-assistant/src/main.rs
index bc7d5d8..2b9b736 100644
--- a/proxmox-auto-install-assistant/src/main.rs
+++ b/proxmox-auto-install-assistant/src/main.rs
@@ -20,6 +20,7 @@ use proxmox_auto_installer::{
         FetchAnswerFrom, HttpOptions,
     },
 };
+use proxmox_installer_common::FIRST_BOOT_EXEC_NAME;
 
 static PROXMOX_ISO_FLAG: &str = "/auto-installer-capable";
 
@@ -150,6 +151,13 @@ struct CommandPrepareISO {
     // so shorten "Automated Installer Source" to "AIS" to be safe.
     #[arg(long, default_value_t = { "proxmox-ais".to_owned() } )]
     partition_label: String,
+
+    /// Executable file to include, which should be run on the first system boot after the
+    /// installation. Can be used for further bootstrapping the new system.
+    ///
+    /// Must be appropriately enabled in the answer file.
+    #[arg(long)]
+    on_first_boot: Option<PathBuf>,
 }
 
 /// Show the system information that can be used to identify a host.
@@ -353,6 +361,15 @@ fn prepare_iso(args: &CommandPrepareISO) -> Result<()> {
         inject_file_to_iso(&tmp_iso, answer_file, "/answer.toml", &uuid)?;
     }
 
+    if let Some(first_boot) = &args.on_first_boot {
+        inject_file_to_iso(
+            &tmp_iso,
+            first_boot,
+            &format!("/{FIRST_BOOT_EXEC_NAME}"),
+            &uuid,
+        )?;
+    }
+
     println!("Moving prepared ISO to target location...");
     fs::rename(&tmp_iso, &iso_target)?;
     println!("Final ISO is available at {iso_target:?}.");
diff --git a/proxmox-installer-common/src/lib.rs b/proxmox-installer-common/src/lib.rs
index 10b5940..c25f105 100644
--- a/proxmox-installer-common/src/lib.rs
+++ b/proxmox-installer-common/src/lib.rs
@@ -11,3 +11,6 @@ pub const RUNTIME_DIR: &str = "/run/proxmox-installer";
 
 /// Default placeholder value for the administrator email address.
 pub const EMAIL_DEFAULT_PLACEHOLDER: &str = "mail@example.invalid";
+
+/// Name of the executable for the first-boot hook.
+pub const FIRST_BOOT_EXEC_NAME: &str = "proxmox-first-boot";
-- 
2.47.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] 6+ messages in thread

* [pve-devel] [RFC PATCH installer 4/5] fix #5579: auto-installer: add optional first-boot hook script
  2024-11-13 13:59 [pve-devel] [RFC PATCH installer 0/5] fix #5579: allow specifying optional first-boot script Christoph Heiss
                   ` (2 preceding siblings ...)
  2024-11-13 13:59 ` [pve-devel] [RFC PATCH installer 3/5] fix #5579: auto-install-assistant: enable baking in first-boot script Christoph Heiss
@ 2024-11-13 13:59 ` Christoph Heiss
  2024-11-13 13:59 ` [pve-devel] [RFC PATCH installer 5/5] fix #5579: install: copy over `proxmox-first-boot` script if present Christoph Heiss
  4 siblings, 0 replies; 6+ messages in thread
From: Christoph Heiss @ 2024-11-13 13:59 UTC (permalink / raw)
  To: pve-devel

Users can specifying an optional file - either fetched from an URL or
backed into the ISO - to execute on the first boot after the
installation, using the 'proxmox-first-boot' oneshot service.

Essentially adds an (optional) `[first-boot]` section to the answer
file. If specified, the `source` key must be at least set, which gives
the location of the hook script.

Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
 proxmox-auto-installer/Cargo.toml             |  2 +-
 proxmox-auto-installer/src/answer.rs          | 27 ++++++++++++
 .../src/bin/proxmox-auto-installer.rs         | 42 +++++++++++++++++--
 proxmox-auto-installer/src/utils.rs           | 15 ++++++-
 4 files changed, 80 insertions(+), 6 deletions(-)

diff --git a/proxmox-auto-installer/Cargo.toml b/proxmox-auto-installer/Cargo.toml
index 21ed538..7e3d90c 100644
--- a/proxmox-auto-installer/Cargo.toml
+++ b/proxmox-auto-installer/Cargo.toml
@@ -13,7 +13,7 @@ homepage = "https://www.proxmox.com"
 [dependencies]
 anyhow.workspace = true
 log.workspace = true
-proxmox-installer-common.workspace = true
+proxmox-installer-common = { workspace = true, features = ["http"] }
 serde = { workspace = true, features = ["derive"] }
 serde_json.workspace = true
 serde_plain.workspace = true
diff --git a/proxmox-auto-installer/src/answer.rs b/proxmox-auto-installer/src/answer.rs
index c23f1f3..23d6878 100644
--- a/proxmox-auto-installer/src/answer.rs
+++ b/proxmox-auto-installer/src/answer.rs
@@ -22,6 +22,8 @@ pub struct Answer {
     pub disks: Disks,
     #[serde(default)]
     pub posthook: Option<PostNotificationHookInfo>,
+    #[serde(default)]
+    pub first_boot: Option<FirstBootHookInfo>,
 }
 
 impl Answer {
@@ -62,6 +64,31 @@ pub struct PostNotificationHookInfo {
     pub cert_fingerprint: Option<String>,
 }
 
+/// Possible sources for the optional first-boot hook script/executable file.
+#[derive(Clone, Deserialize, Debug, PartialEq)]
+#[serde(rename_all = "kebab-case", deny_unknown_fields)]
+pub enum FirstBootHookSourceMode {
+    /// Fetch the executable file from an URL, specified in the parent.
+    FromUrl,
+    /// The executable file has been baked into the ISO at a known location,
+    /// and should be retrieved from there.
+    FromIso,
+}
+
+/// Describes from where to fetch the first-boot hook script, either being baked into the ISO or
+/// from a URL.
+#[derive(Clone, Deserialize, Debug)]
+#[serde(rename_all = "kebab-case", deny_unknown_fields)]
+pub struct FirstBootHookInfo {
+    /// Mode how to retrieve the first-boot executable file, either from an URL or from the ISO if
+    /// it has been baked-in.
+    pub source: FirstBootHookSourceMode,
+    /// Retrieve the post-install script from a URL, if source == "from-url".
+    pub url: Option<String>,
+    /// SHA256 cert fingerprint if certificate pinning should be used, if source == "from-url".
+    pub cert_fingerprint: Option<String>,
+}
+
 #[derive(Clone, Deserialize, Debug, Default, PartialEq)]
 #[serde(deny_unknown_fields)]
 enum NetworkConfigMode {
diff --git a/proxmox-auto-installer/src/bin/proxmox-auto-installer.rs b/proxmox-auto-installer/src/bin/proxmox-auto-installer.rs
index ea45c29..9d8a2e5 100644
--- a/proxmox-auto-installer/src/bin/proxmox-auto-installer.rs
+++ b/proxmox-auto-installer/src/bin/proxmox-auto-installer.rs
@@ -1,18 +1,22 @@
 use anyhow::{bail, format_err, Result};
 use log::{error, info, LevelFilter};
 use std::{
-    env,
+    env, fs,
     io::{BufRead, BufReader, Write},
     path::PathBuf,
     process::ExitCode,
 };
 
-use proxmox_installer_common::setup::{
-    installer_setup, read_json, spawn_low_level_installer, LocaleInfo, RuntimeInfo, SetupInfo,
+use proxmox_installer_common::{
+    http,
+    setup::{
+        installer_setup, read_json, spawn_low_level_installer, LocaleInfo, RuntimeInfo, SetupInfo,
+    },
+    FIRST_BOOT_EXEC_NAME, RUNTIME_DIR,
 };
 
 use proxmox_auto_installer::{
-    answer::Answer,
+    answer::{Answer, FirstBootHookInfo, FirstBootSourceMode},
     log::AutoInstLogger,
     udevinfo::UdevInfo,
     utils::{parse_answer, LowLevelMessage},
@@ -27,6 +31,31 @@ pub fn init_log() -> Result<()> {
         .map_err(|err| format_err!(err))
 }
 
+fn setup_first_boot_executable(first_boot: &FirstBootHookInfo) -> Result<()> {
+    let content = match first_boot.source {
+        FirstBootSourceMode::FromUrl => {
+            if let Some(url) = &first_boot.url {
+                info!("Fetching first-boot hook from {url} ..");
+                Some(http::get(url, first_boot.cert_fingerprint.as_deref())?)
+            } else {
+                bail!("first-boot hook source set to URL, but none specified!");
+            }
+        }
+        FirstBootSourceMode::FromIso => Some(fs::read_to_string(format!(
+            "/cdrom/{FIRST_BOOT_EXEC_NAME}"
+        ))?),
+    };
+
+    if let Some(content) = content {
+        Ok(fs::write(
+            format!("/{RUNTIME_DIR}/{FIRST_BOOT_EXEC_NAME}"),
+            content,
+        )?)
+    } else {
+        Ok(())
+    }
+}
+
 fn auto_installer_setup(in_test_mode: bool) -> Result<(Answer, UdevInfo)> {
     let base_path = if in_test_mode { "./testdir" } else { "/" };
     let mut path = PathBuf::from(base_path);
@@ -43,6 +72,11 @@ fn auto_installer_setup(in_test_mode: bool) -> Result<(Answer, UdevInfo)> {
     };
 
     let answer = Answer::try_from_reader(std::io::stdin().lock())?;
+
+    if let Some(first_boot) = &answer.first_boot {
+        setup_first_boot_executable(first_boot)?;
+    }
+
     Ok((answer, udev_info))
 }
 
diff --git a/proxmox-auto-installer/src/utils.rs b/proxmox-auto-installer/src/utils.rs
index 83f3f12..25f537d 100644
--- a/proxmox-auto-installer/src/utils.rs
+++ b/proxmox-auto-installer/src/utils.rs
@@ -5,7 +5,7 @@ use log::info;
 use std::{collections::BTreeMap, process::Command};
 
 use crate::{
-    answer::{self, Answer},
+    answer::{self, Answer, FirstBootSourceMode},
     udevinfo::UdevInfo,
 };
 use proxmox_installer_common::{
@@ -320,6 +320,18 @@ fn verify_email_and_root_password_settings(answer: &Answer) -> Result<()> {
     }
 }
 
+fn verify_first_boot_settings(answer: &Answer) -> Result<()> {
+    info!("Verifying first boot settings");
+
+    if let Some(first_boot) = &answer.first_boot {
+        if first_boot.source == FirstBootSourceMode::FromUrl && first_boot.url.is_none() {
+            bail!("first-boot executable source set to URL, but none specified!");
+        }
+    }
+
+    Ok(())
+}
+
 pub fn parse_answer(
     answer: &Answer,
     udev_info: &UdevInfo,
@@ -336,6 +348,7 @@ pub fn parse_answer(
 
     verify_locale_settings(answer, locales)?;
     verify_email_and_root_password_settings(answer)?;
+    verify_first_boot_settings(answer)?;
 
     let mut config = InstallConfig {
         autoreboot: 1_usize,
-- 
2.47.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] 6+ messages in thread

* [pve-devel] [RFC PATCH installer 5/5] fix #5579: install: copy over `proxmox-first-boot` script if present
  2024-11-13 13:59 [pve-devel] [RFC PATCH installer 0/5] fix #5579: allow specifying optional first-boot script Christoph Heiss
                   ` (3 preceding siblings ...)
  2024-11-13 13:59 ` [pve-devel] [RFC PATCH installer 4/5] fix #5579: auto-installer: add optional first-boot hook script Christoph Heiss
@ 2024-11-13 13:59 ` Christoph Heiss
  4 siblings, 0 replies; 6+ messages in thread
From: Christoph Heiss @ 2024-11-13 13:59 UTC (permalink / raw)
  To: pve-devel

The auto-installer will place an executable file named
`proxmox-first-boot` in the installer runtime-directory if the user set
up.

Based on the presence of this file, we copy it over to the target system
and set a flag file, indicating to the 'proxmox-first-boot' service that
it is indeed the very first boot of the new system and should run.

Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
 Proxmox/Install.pm | 20 ++++++++++++++++++++
 1 file changed, 20 insertions(+)

diff --git a/Proxmox/Install.pm b/Proxmox/Install.pm
index 5c64c3d..f46d86a 100644
--- a/Proxmox/Install.pm
+++ b/Proxmox/Install.pm
@@ -681,6 +681,7 @@ sub extract_data {
 
     my $proxmox_libdir = $iso_env->{locations}->{lib};
     my $proxmox_cddir = $iso_env->{locations}->{iso};
+    my $proxmox_rundir = $iso_env->{locations}->{run};
     my $proxmox_pkgdir = "${proxmox_cddir}/proxmox/packages/";
 
     my $targetdir = is_test_mode() ? "target" : "/target";
@@ -1241,6 +1242,25 @@ _EOD
 	    debconfig_set($targetdir, "pve-manager pve-manager/country string $ucc\n");
 	}
 
+	my $firstboot_exec_name = 'proxmox-first-boot';
+	if (-f "$proxmox_rundir/$firstboot_exec_name") {
+	    my $firstboot_pending_flagfile = "pending-first-boot-setup";
+	    my $targetpath = "$targetdir/var/lib/proxmox-first-boot";
+
+	    syscmd("mkdir -p $targetpath/") == 0
+		|| die "failed to create $targetpath directory\n";
+
+	    syscmd("cp $proxmox_rundir/$firstboot_exec_name $targetpath/") == 0
+		|| die "unable to copy $firstboot_exec_name executable\n";
+	    syscmd("touch $targetpath/$firstboot_pending_flagfile") == 0
+		|| die "unable to create $firstboot_pending_flagfile flag file\n";
+
+	    # Explicitly mark the entire directory only accessible, to prevent
+	    # possible secret leaks from the bootstrap script.
+	    syscmd("chmod -R 0700 $targetpath") == 0
+		|| warn "failed to set permissions for $targetpath\n";
+	}
+
 	update_progress(0.8, 0.95, 1, "make system bootable");
 	my $target_cmdline='';
 	if ($target_cmdline = Proxmox::Install::Config::get_target_cmdline()) {
-- 
2.47.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] 6+ messages in thread

end of thread, other threads:[~2024-11-13 13:59 UTC | newest]

Thread overview: 6+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2024-11-13 13:59 [pve-devel] [RFC PATCH installer 0/5] fix #5579: allow specifying optional first-boot script Christoph Heiss
2024-11-13 13:59 ` [pve-devel] [RFC PATCH installer 1/5] common: add function for issuing HTTP GET requests Christoph Heiss
2024-11-13 13:59 ` [pve-devel] [RFC PATCH installer 2/5] fix #5579: first-boot: add initial service packaging Christoph Heiss
2024-11-13 13:59 ` [pve-devel] [RFC PATCH installer 3/5] fix #5579: auto-install-assistant: enable baking in first-boot script Christoph Heiss
2024-11-13 13:59 ` [pve-devel] [RFC PATCH installer 4/5] fix #5579: auto-installer: add optional first-boot hook script Christoph Heiss
2024-11-13 13:59 ` [pve-devel] [RFC PATCH installer 5/5] fix #5579: install: copy over `proxmox-first-boot` script if present 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