Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ The table below shows the currently provided operating systems for each provider
| Ubuntu 22.04 | ✅ | 💙 | ✅ | 💙 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | 💙 | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Ubuntu 24.04 | ✅ | 💙 | ✅ | 💙 | ✅ | ❌ | ✅ | ✅ | ❌ | ✅ | ✅ | 💙 | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Windows 2019 | ✅ | 💙 | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Windows 2022 | ❌ | 💙 | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ | | ❌ | ❌ | ❌ | ❌ |
| Windows 2022 | ❌ | 💙 | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ | | ❌ | ❌ | ❌ | ❌ |
| Windows 2025 | ❌ | 💙 | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Windows Annual | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |

Expand Down
50 changes: 49 additions & 1 deletion docs/book/src/capi/providers/proxmox.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ The image build process expects a few things to be in place before the build pro
1. DHCP must be available to assign the packer VM an IP
* The packer proxmox integration currently does not support the ability to assign static IPs, thus DHCP is required.
* Access to internet hosts is optional, but the VM will not be able to apply any current updates and will need to be manually rebooted to get a clean cloud-init status.
2. The build VM must be accessible via SSH from the host running `make build-proxmox...`
2. The build VM must be accessible via SSH or Winrm from the host running `make build-proxmox...`
* Linux builds use SSH.
* Windows builds use WinRM.
3. The build VM must have DHCP, DNS, HTTP, HTTPS and NTP accessibility to successfully update the OS packages.

## Building Images
Expand All @@ -38,6 +40,7 @@ the different operating systems.
|--------------------|-----------------------------------------|
| `ubuntu-2204.json` | The settings for the Ubuntu 22.04 image |
| `ubuntu-2404.json` | The settings for the Ubuntu 24.04 image |
| `windows-2022.json` | The settings for the Windows Server 2022 image |

The full list of available environment vars can be found in the `variables` section of `images/capi/packer/proxmox/packer.json`.

Expand Down Expand Up @@ -84,6 +87,45 @@ export PACKER_FLAGS="--var 'oem_id=qemu'"
make build-proxmox-flatcar
```

### Building images for Windows

Windows builds require a Windows installation ISO and a Windows administrator
password in addition to the standard Proxmox credentials.

Set the following environment variables before running the build:

```bash
export PROXMOX_URL="https://pve.example.com:8006/api2/json"
export PROXMOX_USERNAME=<USERNAME>
export PROXMOX_TOKEN=<TOKEN_ID>
export PROXMOX_NODE="pve"
export PROXMOX_ISO_POOL="local"
export PROXMOX_BRIDGE="vmbr0"
export PROXMOX_STORAGE_POOL="local-lvm"
export ISO_FILE="local:iso/en-us_windows_server_2022_x64.iso"
export WINDOWS_ADMIN_PASSWORD='<PASSWORD>'
```

The Proxmox Windows build also expects a VirtIO driver ISO to be available in the
selected ISO storage. By default the build uses:

```bash
local:iso/virtio-win-0.1.285.iso
```

If your environment uses a different VirtIO image name or location, override it
with `PACKER_FLAGS`:

```bash
export PACKER_FLAGS="--var 'iso_virtio=local:iso/virtio-win-0.1.285.iso'"
```

Build the Windows Server 2022 template with:

```bash
make build-proxmox-windows-2022
```

### Example

Prior to building images you need to ensure you have set the required environment variables:
Expand All @@ -104,6 +146,12 @@ export PROXMOX_STORAGE_POOL="local-lvm"
make build-proxmox-ubuntu-2204
```

- Build Windows Server 2022 template:

```bash
make build-proxmox-windows-2022
```

### Note on disk formats

Depending on what storage type you are using, you will need to change the default disk
Expand Down
14 changes: 12 additions & 2 deletions images/capi/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ deps-huaweicloud: deps-common
.PHONY: deps-proxmox
deps-proxmox: ## Installs/checks dependencies for Proxmox builds
deps-proxmox: deps-common
hack/ensure-ansible-windows.sh
$(PACKER) init packer/config.pkr.hcl
$(PACKER) init packer/proxmox/config.pkr.hcl

Expand Down Expand Up @@ -403,7 +404,7 @@ NUTANIX_BUILD_NAMES ?= nutanix-ubuntu-2204 nutanix-ubuntu-2404 nutanix-rhel-9 nu

HCLOUD_BUILD_NAMES ?= hcloud-ubuntu-2204 hcloud-ubuntu-2404 hcloud-rockylinux-9 hcloud-flatcar hcloud-flatcar-arm64

PROXMOX_BUILD_NAMES ?= proxmox-ubuntu-2204 proxmox-ubuntu-2404 proxmox-ubuntu-2404-efi proxmox-rockylinux-9 proxmox-flatcar
PROXMOX_BUILD_NAMES ?= proxmox-ubuntu-2204 proxmox-ubuntu-2404 proxmox-ubuntu-2404-efi proxmox-rockylinux-9 proxmox-flatcar proxmox-windows-2022

VULTR_BUILD_NAMES ?= vultr-ubuntu-2204 vultr-ubuntu-2404

Expand Down Expand Up @@ -644,7 +645,11 @@ $(HCLOUD_VALIDATE_TARGETS): deps-hcloud

.PHONY: $(PROXMOX_BUILD_TARGETS)
$(PROXMOX_BUILD_TARGETS): deps-proxmox set-ssh-password
$(PACKER) build $(PACKER_NODE_FLAGS) -var-file="$(abspath packer/proxmox/$(subst build-proxmox-,,$@).json)" $(ABSOLUTE_PACKER_VAR_FILES) packer/proxmox/packer.json
# This uses a packer file builder to input unattend variables into a JSON file to be consumed by the python script before running the vsphere provisioner
$(if $(findstring windows,$@),$(PACKER) build $(PACKER_WINDOWS_NODE_FLAGS) -var-file="$(abspath packer/proxmox/$(subst build-proxmox-,,$@).json)" -var-file="$(abspath packer/proxmox/$(subst build-proxmox-,,$@).json)" -only=file $(ABSOLUTE_PACKER_VAR_FILES) packer/proxmox/packer-windows.json,)
$(if $(findstring windows,$@),hack/windows-unattend.py --unattend-file='./packer/proxmox/windows/$(subst build-proxmox-,,$@)/autounattend.xml',)
$(PACKER) build $(if $(findstring windows,$@),$(PACKER_WINDOWS_NODE_FLAGS),$(PACKER_NODE_FLAGS)) -var-file="$(abspath packer/proxmox/$(subst build-proxmox-,,$@).json)" $(ABSOLUTE_PACKER_VAR_FILES) packer/proxmox/packer$(if $(findstring windows,$@),-windows,).json

Comment on lines +648 to +652
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This section looks copy-pasted: the same -var-file is passed twice, and the comment references vSphere in the Proxmox path. Is the duplicate var-file intentional?


.PHONY: $(PROXMOX_VALIDATE_TARGETS)
$(PROXMOX_VALIDATE_TARGETS): deps-proxmox set-ssh-password
Expand Down Expand Up @@ -859,6 +864,11 @@ build-proxmox-ubuntu-2404: ## Builds Ubuntu 24.04 Proxmox image
build-proxmox-ubuntu-2404-efi: ## Builds Ubuntu 24.04 Proxmox image that EFI boots
build-proxmox-rockylinux-9: ## Builds Rocky Linux 9 Proxmox image
build-proxmox-flatcar: ## Builds Flatcar Proxmox image
build-proxmox-ubuntu-2204: ## Builds the Proxmox ubuntu-2204 image
build-proxmox-ubuntu-2404: ## Builds the Proxmox ubuntu-2404 image
build-proxmox-ubuntu-2404-efi: ## Builds the Proxmox ubuntu-2404-efi image that EFI boots
build-proxmox-rockylinux-9: ## Builds the Proxmox rockylinux-9 image
Comment on lines +867 to +870
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Look like duplicate lines of 862 - 865

build-proxmox-windows-2022: ## Builds the Proxmox Windows 2022 image
build-proxmox-all: $(PROXMOX_BUILD_TARGETS) ## Builds all Proxmox images

build-vultr-ubuntu-2204: ## Builds Ubuntu 22.04 Vultr Snapshot
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,12 @@ netbios_host_name_compatibility={{ netbios_host_name_compatibility }}

metadata_services={{ cloudbase_metadata_services }}
plugins={{ cloudbase_plugins }}
{% if (cloudbase_nocloud_metadata_file | default('') | length > 0) or (cloudbase_nocloud_networkdata_file | default('') | length > 0) or (cloudbase_nocloud_userdata_file | default('') | length > 0) %}

[nocloud]
metadata_file={{ cloudbase_nocloud_metadata_file | default('meta-data', true) }}
{% if cloudbase_nocloud_networkdata_file | default('') | length > 0 %}
networkdata_file={{ cloudbase_nocloud_networkdata_file }}
{% endif %}
userdata_file={{ cloudbase_nocloud_userdata_file | default('user-data', true) }}
{% endif %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# Copyright 2026 The Kubernetes Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Apply Proxmox NoCloud network data from a config-drive file."""

import logging
import os
import string
import sys

try:
from oslo_log import log as oslo_logging

LOG = oslo_logging.getLogger(__name__)
except Exception: # pragma: no cover - fallback when oslo logging is unavailable
logging.basicConfig(level=logging.INFO)
LOG = logging.getLogger(__name__)


DEFAULT_NETWORK_DATA_FILENAMES = ("NETWORK_CONFIG", "network-config")


def _iter_search_roots():
search_roots = os.environ.get("PROXMOX_NETWORK_DATA_SEARCH_ROOTS")
if search_roots:
for root in search_roots.split(os.pathsep):
root = root.strip()
if root:
yield root
return

for drive_letter in string.ascii_uppercase:
yield "%s:\\" % drive_letter


def _iter_candidate_paths():
override_path = os.environ.get("PROXMOX_NETWORK_DATA_PATH", "").strip()
if override_path:
yield override_path
return

for root in _iter_search_roots():
normalized_root = root.rstrip("\\/")
for filename in DEFAULT_NETWORK_DATA_FILENAMES:
yield "%s\\%s" % (normalized_root, filename)


def find_network_data_path(path_exists=os.path.exists):
for candidate_path in _iter_candidate_paths():
if path_exists(candidate_path):
return candidate_path
return None


def load_network_data(network_data_path, open_file=open, parser=None):
if parser is None:
from cloudbaseinit.utils import serialization

parser = serialization.parse_json_yaml

with open_file(network_data_path, "r", encoding="utf-8") as network_data_file:
raw_network_data = network_data_file.read()

network_data = parser(raw_network_data)
if not isinstance(network_data, dict):
raise ValueError(
"Proxmox network data parsed into %r, expected dict" %
type(network_data)
)

return network_data


def apply_network_data(network_data, network_parser=None, plugin_factory=None):
if network_parser is None:
from cloudbaseinit.metadata.services.nocloudservice import (
NoCloudNetworkConfigParser,
)

network_parser = NoCloudNetworkConfigParser.parse

if plugin_factory is None:
from cloudbaseinit.plugins.common import networkconfig

plugin_factory = networkconfig.NetworkConfigPlugin

network_details = network_parser(network_data)
if not network_details:
LOG.warning("NoCloud network parser returned no interfaces")
return False

plugin = plugin_factory()
process_network_details = getattr(plugin, "_process_network_details_v2", None)
if process_network_details is None:
raise AttributeError(
"Cloudbase-Init network plugin is missing _process_network_details_v2"
)

process_network_details(network_details)
return True


def main():
network_data_path = find_network_data_path()
if not network_data_path:
LOG.info(
"No Proxmox network data found in candidate paths: %s",
", ".join(_iter_candidate_paths()),
)
return 0

try:
network_data = load_network_data(network_data_path)
except Exception:
LOG.exception(
"Failed to load Proxmox network data from %s", network_data_path
)
return 0

try:
LOG.info("Applying Proxmox network data from %s", network_data_path)
applied = apply_network_data(network_data)
except Exception:
LOG.exception(
"Failed to apply Proxmox network data from %s", network_data_path
)
return 0

if not applied:
LOG.warning(
"No network interfaces were applied from %s", network_data_path
)

return 0
Comment on lines +114 to +145
Copy link
Copy Markdown
Contributor

@drew-viles drew-viles Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it not be better to fail a little louder rather than return 0 if something fails? As I'm reading this, it looks like failures will happen silently meaning an image could successfully build even if something like applying the proxmox network fails.

Would we be ok with this?

Copy link
Copy Markdown
Author

@plouton24 plouton24 Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this is kind of my hack to read the cloudinit network data that CAPMOX injects via a ISO-9660 cd drive for kubeadm boostrap. So this wont fail the build of the VM, as its a node first boot helper script for cloudbase-init(windows version of cloud-init). Though I should try to write to a file to troubleshoot networking incase CAPMOX changes its kubeadmbootstrap process, and be more verbose on failures.



if __name__ == "__main__":
sys.exit(main())
3 changes: 3 additions & 0 deletions images/capi/ansible/windows/roles/providers/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,8 @@
- ansible.builtin.include_tasks: azure.yml
when: packer_builder_type.startswith('azure')

- ansible.builtin.include_tasks: proxmox.yml
when: packer_builder_type is search('proxmox')

- ansible.builtin.include_tasks: vmware.yml
when: packer_builder_type is search('vmware') or packer_builder_type is search('vsphere')
23 changes: 23 additions & 0 deletions images/capi/ansible/windows/roles/providers/tasks/proxmox.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Copyright 2026 The Kubernetes Authors.

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at

# http://www.apache.org/licenses/LICENSE-2.0

# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
---
- name: Ensure Proxmox Cloudbase-Init LocalScripts directory exists
ansible.windows.win_file:
path: "{{ programfiles.stdout | trim }}\\Cloudbase Solutions\\Cloudbase-Init\\LocalScripts"
state: directory

- name: Copy Proxmox Cloudbase-Init compatibility helper
ansible.windows.win_copy:
src: proxmox/cloudbase_helper.py
dest: "{{ programfiles.stdout | trim }}\\Cloudbase Solutions\\Cloudbase-Init\\LocalScripts\\cloudbase_helper.py"
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"ansible_common_vars": "runtime={{user `runtime`}} containerd_url={{user `containerd_url`}} containerd_sha256={{user `containerd_sha256_windows`}} containerd_version={{user `containerd_version`}} pause_image={{user `pause_image`}} additional_debug_files=\"{{user `additional_debug_files`}}\" containerd_additional_settings={{user `containerd_additional_settings`}} custom_role_names=\"{{user `custom_role_names`}}\" http_proxy={{user `http_proxy`}} https_proxy={{user `https_proxy`}} no_proxy={{user `no_proxy`}} kubernetes_base_url={{user `kubernetes_base_url`}} kubernetes_semver={{user `kubernetes_semver`}} kubernetes_install_path={{user `kubernetes_install_path`}} cloudbase_init_url=\"{{user `cloudbase_init_url`}}\" cloudbase_plugins=\"{{user `cloudbase_plugins`}}\" cloudbase_metadata_services=\"{{user `cloudbase_metadata_services`}}\" cloudbase_plugins_unattend=\"{{user `cloudbase_plugins_unattend`}}\" cloudbase_metadata_services_unattend=\"{{user `cloudbase_metadata_services_unattend`}}\" prepull={{user `prepull`}} windows_updates_kbs=\"{{user `windows_updates_kbs`}}\" windows_updates_categories=\"{{user `windows_updates_categories`}}\" windows_service_manager={{user `windows_service_manager`}} nssm_url={{user `nssm_url`}} distribution_version={{user `distribution_version`}} netbios_host_name_compatibility={{user `netbios_host_name_compatibility`}} disable_hypervisor={{ user `disable_hypervisor` }} cloudbase_logging_serial_port={{ user `cloudbase_logging_serial_port` }} cloudbase_real_time_clock_utc={{ user `cloudbase_real_time_clock_utc` }} load_additional_components={{ user `load_additional_components`}} ecr_credential_provider={{ user `ecr_credential_provider` }} additional_registry_images={{ user `additional_registry_images`}} additional_registry_images_list={{ user `additional_registry_images_list`}} additional_url_images={{ user `additional_url_images`}} additional_url_images_list={{ user `additional_url_images_list`}} additional_executables={{ user `additional_executables`}} additional_executables_list={{ user `additional_executables_list`}} additional_executables_destination_path={{ user `additional_executables_destination_path`}} ssh_source_url={{user `ssh_source_url` }} debug_tools={{user `debug_tools`}}"
"ansible_common_vars": "runtime={{user `runtime`}} containerd_url={{user `containerd_url`}} containerd_sha256={{user `containerd_sha256_windows`}} containerd_version={{user `containerd_version`}} pause_image={{user `pause_image`}} additional_debug_files=\"{{user `additional_debug_files`}}\" containerd_additional_settings={{user `containerd_additional_settings`}} custom_role_names=\"{{user `custom_role_names`}}\" http_proxy={{user `http_proxy`}} https_proxy={{user `https_proxy`}} no_proxy={{user `no_proxy`}} kubernetes_base_url={{user `kubernetes_base_url`}} kubernetes_semver={{user `kubernetes_semver`}} kubernetes_install_path={{user `kubernetes_install_path`}} cloudbase_init_url=\"{{user `cloudbase_init_url`}}\" cloudbase_plugins=\"{{user `cloudbase_plugins`}}\" cloudbase_metadata_services=\"{{user `cloudbase_metadata_services`}}\" cloudbase_plugins_unattend=\"{{user `cloudbase_plugins_unattend`}}\" cloudbase_metadata_services_unattend=\"{{user `cloudbase_metadata_services_unattend`}}\" cloudbase_nocloud_metadata_file=\"{{user `cloudbase_nocloud_metadata_file`}}\" cloudbase_nocloud_networkdata_file=\"{{user `cloudbase_nocloud_networkdata_file`}}\" cloudbase_nocloud_userdata_file=\"{{user `cloudbase_nocloud_userdata_file`}}\" prepull={{user `prepull`}} windows_updates_kbs=\"{{user `windows_updates_kbs`}}\" windows_updates_categories=\"{{user `windows_updates_categories`}}\" windows_service_manager={{user `windows_service_manager`}} nssm_url={{user `nssm_url`}} distribution_version={{user `distribution_version`}} netbios_host_name_compatibility={{user `netbios_host_name_compatibility`}} disable_hypervisor={{ user `disable_hypervisor` }} cloudbase_logging_serial_port={{ user `cloudbase_logging_serial_port` }} cloudbase_real_time_clock_utc={{ user `cloudbase_real_time_clock_utc` }} load_additional_components={{ user `load_additional_components`}} ecr_credential_provider={{ user `ecr_credential_provider` }} additional_registry_images={{ user `additional_registry_images`}} additional_registry_images_list={{ user `additional_registry_images_list`}} additional_url_images={{ user `additional_url_images`}} additional_url_images_list={{ user `additional_url_images_list`}} additional_executables={{ user `additional_executables`}} additional_executables_list={{ user `additional_executables_list`}} additional_executables_destination_path={{ user `additional_executables_destination_path`}} ssh_source_url={{user `ssh_source_url` }} debug_tools={{user `debug_tools`}}"
}
3 changes: 3 additions & 0 deletions images/capi/packer/config/windows/common.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
{
"additional_debug_files": "",
"cloudbase_nocloud_metadata_file": "",
"cloudbase_nocloud_networkdata_file": "",
"cloudbase_nocloud_userdata_file": "",
"debug_tools": "true",
"disable_hypervisor": "false",
"http_proxy": "",
Expand Down
29 changes: 29 additions & 0 deletions images/capi/packer/goss/goss-vars.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -694,3 +694,32 @@ windows:
filetype: file
contains:
- "metadata_services=cloudbaseinit.metadata.services.base.EmptyMetadataService"

proxmox:
windows-service:

files:
'c:/program files/Cloudbase Solutions/Cloudbase-init/conf/cloudbase-init.conf':
exists: true
filetype: file
contains:
- "!/logging_serial_port=COM1,115200,N,8/"
- "metadata_services=cloudbaseinit.metadata.services.nocloudservice.NoCloudConfigDriveService"
- "cloudbaseinit.plugins.common.ephemeraldisk.EphemeralDiskPlugin"
- "cloudbaseinit.plugins.common.mtu.MTUPlugin"
- "cloudbaseinit.plugins.common.sethostname.SetHostNamePlugin"
- "cloudbaseinit.plugins.common.networkconfig.NetworkConfigPlugin"
- "cloudbaseinit.plugins.common.sshpublickeys.SetUserSSHPublicKeysPlugin"
- "cloudbaseinit.plugins.common.userdata.UserDataPlugin"
- "cloudbaseinit.plugins.common.localscripts.LocalScriptsPlugin"
- "cloudbaseinit.plugins.windows.createuser.CreateUserPlugin"
- "cloudbaseinit.plugins.windows.extendvolumes.ExtendVolumesPlugin"
- "[nocloud]"
- "metadata_file=META_DATA"
- "networkdata_file=NETWORK_CONFIG"
- "userdata_file=USER_DATA"
'c:/program files/Cloudbase Solutions/Cloudbase-init/localscripts/cloudbase_helper.py':
exists: true
filetype: file
contains:
- "Apply Proxmox NoCloud network data from a config-drive file."
Loading