xcp-ng-ce-iso

ISO assembly pipeline — takes the community RPM builds and releases a bootable XCP-ng HL ISO.

Repository: Vagrantin/xcp-ng-ce-iso · Language: Bash / YAML · License: AGPL-3.0

Table of contents

  1. Purpose
  2. Toolchain — create-install-image
  3. Repo structure
    1. CUSTOMREPO.tmpl
    2. Community RPM repository layout
  4. How packages reach the installed host
  5. Docker build environment
  6. Build process — step by step
    1. 1. Prepare the community repo
    2. 2. Inject the community public key into the installer chroot
    3. 3. Run create-installimg.sh (root)
    4. 4. Stamp the ISO build number
    5. 5. Run create-iso.sh (non-root)
    6. 6. isohybrid post-processing
    7. 7. Checksum and sign
    8. 8. Verifying a release (end user procedure)
  7. install.img — internals
    1. Unpacking and repacking
    2. answerfile.xml injection (optional)
  8. CI workflow (GitHub Actions)
  9. Contributing

Purpose

This repo takes the signed xo-lite-ce RPM produced by xolite-ce and the signed xoa-proxy RPM produced by xoa-proxy, overlays them onto a stock XCP-ng 8.3 base using the official create-install-image toolchain, and publishes the resulting ISO as a GitHub Release.


Toolchain — create-install-image

XCP-ng’s official installer ISO is assembled with the create-install-image toolchain. XCP-ng HL uses it directly rather than maintaining a fork.

The toolchain provides two scripts:

Script Runs as Produces
create-installimg.sh root install.img (SquashFS ramdisk)
create-iso.sh non-root final .iso file

They must be run separately, in order. create-iso.sh accepts a --sign-script argument for MD5 imprinting.


Repo structure

xcp-ng-ce-iso/
├── configs/
│   ├── base/
│   │   └── CUSTOMREPO.tmpl     ← community repo template
│   └── 8.3/                    ← XCP-ng 8.3 config (target)
├── community-repo/
│   └── x86_64/
│       └── repodata/           ← generated by createrepo_c
├── scripts/
│   └── debug/                  ← debug helper scripts (overlaid in CI)
├── Dockerfile.build             ← build environment
└── .github/workflows/
    └── build-iso.yml

CUSTOMREPO.tmpl

The community repo definition template must be copied from configs/base/ to configs/8.3/ before the build runs:

cp configs/base/CUSTOMREPO.tmpl configs/8.3/CUSTOMREPO.tmpl

This file tells the installer where to find the community RPM repo. The --define-repo flag passes it to both build scripts.

Community RPM repository layout

yum automatically appends the host architecture (x86_64/) to the base URL configured in a repo file. Therefore the community repo must be structured with repodata/ under the arch subdirectory:

community-repo/
└── x86_64/
    ├── xo-lite-ce-*.rpm
    ├── xoa-proxy-*.rpm
    ├── xcp-hl-release-*.rpm
    └── repodata/
        ├── repomd.xml
        └── ...

Generate the repo metadata against the arch subdirectory:

createrepo_c community-repo/x86_64/

How packages reach the installed host

install.img is the installer’s own ramdisk, not the installed host’s filesystem. Adding a package to packages.lst puts it in the installer environment and nowhere else.

The installed host is built by host-installer from the ISO’s Packages/ directory, and it installs xcp-ng-deps plus that package’s dependency closure. Staging an RPM on the media is therefore not enough on its own — if nothing in the closure requires it, it sits in the ISO repo unused.

The community packages ride in on this chain:

xcp-ng-deps
    └── requires xo-lite ──► provided by xo-lite-ce
                                 ├── requires xoa-proxy
                                 └── requires xcp-hl-release

That Requires: is the only route in. xcp-hl-release used to be staged by name with --extra-packages as well, but that flag only places a package in the ISO’s repo without selecting it for install, so once the dependency existed it was redundant and only obscured which mechanism delivered the package.

A consequence worth knowing: pinning an xolite-ce release from before the Requires: was added produces an ISO with no xcp-hl-release on it. The build’s verification step catches that rather than shipping it.

Verify on a host installed from the ISO:

rpm -q xcp-hl-release
ls /etc/yum.repos.d/xcp-hl.repo

Docker build environment

The build runs inside xcp-ng-build-env:8.3, committed as xcp-ng-build-ready after initial setup:

# First run: commit the prepared image
docker run --name xcpng-build xcp-ng/xcp-ng-build-env:8.3 /bin/true
docker commit xcpng-build xcp-ng-build-ready

Build process — step by step

1. Prepare the community repo

# Download signed RPM from xolite-ce latest release
gh release download --repo Vagrantin/xolite-ce \
    --pattern "xo-lite-ce-*.rpm" \
    --dir community-repo/x86_64/

# Download signed RPM from xoa-proxy latest release
gh release download --repo Vagrantin/xoa-proxy \
    --pattern "xoa-proxy-*.rpm" \
    --dir community-repo/x86_64/

# Generate repo metadata
createrepo_c community-repo/x86_64/

2. Inject the community public key into the installer chroot

The ISO signing subkey (GPG_PRIVATE_KEY in this repo) is imported into the runner’s keyring at the start of the pipeline. The public half is then exported from that keyring and injected into the installer chroot templates, so the installer can verify the community RPMs during installation without any manual key import by the end user.

rpm --import inside a container writes to the container’s RPM database, not to the chroot installroot that ends up in install.img. Always pass --root to target the correct location.

# Export the public key from the runner keyring
gpg --armor --export "${GPG_KEY_ID}" > /tmp/RPM-GPG-KEY-xcp-ng-ce

# Copy into the toolchain's installroot template
mkdir -p create-install-image/templates/installimg/base/etc/pki/rpm-gpg/
cp /tmp/RPM-GPG-KEY-xcp-ng-ce \
   create-install-image/templates/installimg/base/etc/pki/rpm-gpg/

# Import into the installer chroot at build time
rpm --root="${ROOTFS}" --import /etc/pki/rpm-gpg/RPM-GPG-KEY-xcp-ng-ce

3. Run create-installimg.sh (root)

# Must run as root inside the build container
sudo ./create-installimg.sh \
    --define-repo base \
    --define-repo updates \
    --define-repo community \
    [other options]

The --define-repo flag must list all three repos: base, updates, and community.

4. Stamp the ISO build number

host-installer reads [build] number from the media’s .treeinfo and writes it to BUILD_NUMBER in /etc/xensource-inventory on the installed host. Upstream ships the placeholder cloud, and nothing else on an installed host records which media it came from — the volume label does not survive the install, and the logs kept in /var/log/installer/ do not mention it.

Stamp the ce counter into the template before create-iso.sh copies it into the ISO. Only @@...@@ tokens get substituted, so a literal value passes through untouched:

sed -i "s/^number = .*/number = ${CE_COUNTER}/" \
    create-install-image/templates/iso/8.3/.treeinfo

On a host installed from the resulting ISO:

$ grep BUILD_NUMBER /etc/xensource-inventory
BUILD_NUMBER='ce23'

# xapi reads that inventory, so it is visible remotely too
$ xe host-param-get uuid=<host-uuid> param-name=software-version
... build_number: ce23; ...

5. Run create-iso.sh (non-root)

./create-iso.sh \
    --sign-script implantisomd5 \
    --define-repo base \
    --define-repo updates \
    --define-repo community \
    [other options]

6. isohybrid post-processing

This step is required for the ISO to boot on physical hardware that doesn’t support UEFI.

isohybrid --uefi output.iso

Verify the result with:

fdisk -l output.iso
xorriso -report_el_torito output.iso

7. Checksum and sign

Each release ships three files alongside the ISO. The checksum file is named after the ISO it covers:

xcp-ng-ce-8.3.iso
xcp-ng-ce-8.3.iso.sha256
xcp-ng-ce-8.3.iso.sha256.asc

The checksum file contains the SHA256 hash of the ISO. The .asc file is a GPG detached signature over the checksum file, produced with the ISO signing subkey. Together they form a two-step verification chain:

ISO signing subkey
    └── signs ──► xcp-ng-ce-8.3.iso.sha256   (contains hash of the ISO)
                      └── hash matches ──► xcp-ng-ce-8.3.iso
# Produce the checksum file
sha256sum xcp-ng-ce-8.3.iso > xcp-ng-ce-8.3.iso.sha256

# Sign it with the ISO signing subkey
gpg --batch --pinentry-mode loopback \
    --detach-sign --armor \
    xcp-ng-ce-8.3.iso.sha256
# → produces xcp-ng-ce-8.3.iso.sha256.asc

8. Verifying a release (end user procedure)

All three files must be downloaded into the same directory before verifying. sha256sum -c looks for the ISO by filename in the current directory.

# 1. Download all three files into the same directory
cd ~/Downloads
# download xcp-ng-ce-8.3.iso, xcp-ng-ce-8.3.iso.sha256,
#          xcp-ng-ce-8.3.iso.sha256.asc from the GitHub release page

# 2. Import the community GPG key (one-time setup)
gpg --keyserver keys.openpgp.org \
    --recv-keys 2F591DB9D2C128C4C3D963F46DA00DCA5BBA215A

Step 3 — verify the checksum file was signed by this project:

gpg --verify xcp-ng-ce-8.3.iso.sha256.asc \
             xcp-ng-ce-8.3.iso.sha256

Expected output (the important line is Good signature):

gpg: Signature made ...
gpg: Good signature from "XCP-ng home lab Edition <xcp-ng-ce.lid530@passmail.com>"

If you see BAD signature the checksum file has been tampered with — do not proceed.

Step 4 — verify the ISO matches the signed checksum:

sha256sum -c xcp-ng-ce-8.3.iso.sha256

Expected output:

xcp-ng-ce-8.3.iso: OK

If you see FAILED the ISO file is corrupted or was replaced — delete it and re-download.


install.img — internals

install.img is a bzip2-compressed cpio (newc) archive, not SquashFS. create-installimg.sh builds it with find . | cpio -o -H newc | bzip2, so unsquashfs/mksquashfs do not apply.

It holds the installer’s ramdisk, not the installed host’s filesystem — see How packages reach the installed host.

Unpacking and repacking

# Unpack
mkdir installimg-root
bzip2 -dc install.img | (cd installimg-root && cpio -idm)

# Make changes inside installimg-root/
# ...

# Repack (matching upstream: cpio newc, bzip2)
(cd installimg-root && find . | cpio -o -H newc) | bzip2 > install.img.new

List the contents without unpacking:

bzip2 -dc install.img | cpio -it

answerfile.xml injection (optional)

answerfile.xml referenced via file:/// in the installer resolves to the ramdisk root (from install.img), not the ISO CD-ROM root. To inject an answerfile for unattended installs, place it inside install.img via the repack workflow above.

Disable GPG-related checks via answerfile attributes:

<installation gpgcheck="false" repo-gpgcheck="false">
  ...
</installation>

CI workflow (GitHub Actions)

The workflow is dispatch-only. The iso-agent pushes the release tag and then dispatches the workflow on that tag ref, passing the exact component release tags to bake in. A plain tag push no longer triggers a build: resolving components via releases/latest raced with freshly published component releases and could bake a stale RPM.

Key environment requirements:

env:
  TMPDIR: /tmp       # Must be set here AND in Dockerfile
  HOME: /tmp         # Required for non-root create-iso.sh step

Key steps:

- name: Derive versions from tag
  run: |
    TAG="$"                            # e.g. v8.3-ce4
    XCPNG_VER=$(echo "${TAG}" | sed 's/^v//;s/-ce.*//')    # → 8.3
    echo "XCPNG_VER=${XCPNG_VER}" >> $GITHUB_ENV

- name: Import ISO signing key
  # GPG_PRIVATE_KEY in this repo holds the ISO signing subkey —
  # different key material from GPG_PRIVATE_KEY in xolite-ce / xoa-proxy
  run: |
    echo "$" | gpg --batch \
      --pinentry-mode loopback \
      --passphrase "$" --import

- name: Export public key for installer chroot
  run: |
    gpg --armor --export "${GPG_KEY_ID}" > /tmp/RPM-GPG-KEY-xcp-ng-ce
    cp /tmp/RPM-GPG-KEY-xcp-ng-ce \
       create-install-image/templates/installimg/base/etc/pki/rpm-gpg/

- name: Set up Docker build environment
  run: docker build -t xcp-ng-build-ready -f Dockerfile.build .

- name: Download xolite-ce RPM
  run: |
    gh release download --repo Vagrantin/xolite-ce \
      --pattern "xo-lite-ce-*.rpm" \
      --dir community-repo/x86_64/

- name: Download xoa-proxy RPM
  run: |
    gh release download --repo Vagrantin/xoa-proxy \
      --pattern "xoa-proxy-*.rpm" \
      --dir community-repo/x86_64/

- name: Prepare community repo
  run: createrepo_c community-repo/x86_64/

- name: Copy CUSTOMREPO.tmpl
  run: cp configs/base/CUSTOMREPO.tmpl configs/8.3/

- name: Run create-installimg.sh (root)
  run: |
    docker run --rm --privileged \
      -v $PWD:/build \
      -v $PWD/create-install-image:/create-install-image \
      xcp-ng-build-ready \
      bash -c "cd /create-install-image && sudo ./create-installimg.sh ..."

- name: Run create-iso.sh (non-root)
  run: |
    docker run --rm \
      -e HOME=/tmp \
      -v $PWD:/build \
      xcp-ng-build-ready \
      bash -c "cd /create-install-image && ./create-iso.sh ..."

- name: Restore working directory ownership
  run: sudo chown -R $(id -u):$(id -g) .

- name: isohybrid post-processing
  run: isohybrid --uefi output.iso

- name: implantisomd5
  run: implantisomd5 output.iso

- name: Generate and sign checksum
  run: |
    CHECKSUM_FILE="${OUTPUT}.sha256"
    sha256sum "${OUTPUT}" > "${CHECKSUM_FILE}"
    echo "CHECKSUM_FILE=${CHECKSUM_FILE}" >> $GITHUB_ENV
    gpg --batch --pinentry-mode loopback \
      --detach-sign --armor "${CHECKSUM_FILE}"
    gpg --verify "${CHECKSUM_FILE}.asc" "${CHECKSUM_FILE}"

- name: Publish GitHub Release
  uses: softprops/action-gh-release@v2
  with:
    tag_name: $
    files: |
      $
      $
      $.asc

- name: Remove GPG private key
  if: always()
  run: shred -u /tmp/community-signing.key || true

The Docker volume mount must reference create-install-image. Verify the mount target matches the actual cloned directory name.


Contributing

  1. Fork Vagrantin/xcp-ng-ce-iso.
  2. Test changes using the local Docker workflow above.
  3. Enable DEBUG_BUILD variable in your fork’s GitHub Actions settings to activate debug script overlay for CI troubleshooting.
  4. Open a pull request against main.

XCP-ng Community Edition — GPL-3.0 license. Not affiliated with Vates SAS or the XCP-ng project.

This site uses Just the Docs, a documentation theme for Jekyll.