├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_requests.md │ └── question.md ├── PULL_REQUEST_TEMPLATE.md ├── actions │ └── build-and-persist-plugin-binary │ │ └── action.yml ├── dependabot.yml ├── release.yml └── workflows │ ├── build_plugin_binaries.yml │ ├── go-test-darwin.yml │ ├── go-test-linux.yml │ ├── go-test-windows.yml │ ├── go-validate.yml │ ├── notify-integration-release-via-manual.yaml │ ├── notify-integration-release-via-tag.yaml │ └── release.yml ├── .gitignore ├── .go-version ├── .golangci.yml ├── .goreleaser.yml ├── .web-docs ├── README.md ├── components │ └── builder │ │ ├── clone │ │ └── README.md │ │ └── iso │ │ └── README.md ├── metadata.hcl └── scripts │ └── compile-to-webdocs.sh ├── CHANGELOG.md ├── CODEOWNERS ├── GNUmakefile ├── LICENSE ├── README.md ├── builder └── proxmox │ ├── clone │ ├── builder.go │ ├── config.go │ ├── config.hcl2spec.go │ ├── config_test.go │ ├── step_map_source_disks.go │ └── step_ssh_key_pair.go │ ├── common │ ├── artifact.go │ ├── bootcommand_driver.go │ ├── builder.go │ ├── client.go │ ├── client_test.go │ ├── config.go │ ├── config.hcl2spec.go │ ├── config_test.go │ ├── step_convert_to_template.go │ ├── step_convert_to_template_test.go │ ├── step_download_iso_on_pve.go │ ├── step_finalize_template_config.go │ ├── step_finalize_template_config_test.go │ ├── step_remove_cloud_init_drive.go │ ├── step_remove_cloud_init_drive_test.go │ ├── step_start_vm.go │ ├── step_start_vm_test.go │ ├── step_success.go │ ├── step_type_boot_command.go │ ├── step_type_boot_command_test.go │ ├── step_upload_iso.go │ └── step_upload_iso_test.go │ └── iso │ ├── builder.go │ ├── config.go │ ├── config.hcl2spec.go │ ├── config_test.go │ └── testdata │ └── test.iso ├── docs-partials └── builder │ └── proxmox │ ├── clone │ ├── Config-not-required.mdx │ ├── Config-required.mdx │ ├── cloudInitIpconfig-not-required.mdx │ └── cloudInitIpconfig.mdx │ ├── common │ ├── Config-not-required.mdx │ ├── Config.mdx │ ├── ISOsConfig-not-required.mdx │ ├── ISOsConfig.mdx │ ├── NICConfig-not-required.mdx │ ├── NICConfig.mdx │ ├── diskConfig-not-required.mdx │ ├── diskConfig.mdx │ ├── efiConfig-not-required.mdx │ ├── efiConfig.mdx │ ├── pciDeviceConfig-not-required.mdx │ ├── pciDeviceConfig.mdx │ ├── rng0Config-not-required.mdx │ ├── rng0Config-required.mdx │ ├── rng0Config.mdx │ ├── tpmConfig-not-required.mdx │ ├── tpmConfig.mdx │ ├── vgaConfig-not-required.mdx │ └── vgaConfig.mdx │ └── iso │ ├── Config-not-required.mdx │ └── Config-required.mdx ├── docs ├── README.md └── builders │ ├── clone.mdx │ └── iso.mdx ├── go.mod ├── go.sum ├── main.go └── version └── version.go /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: You're experiencing an issue with this Packer plugin that is different than the documented behavior. 4 | labels: bug 5 | --- 6 | 7 | When filing a bug, please include the following headings if possible. Any 8 | example text in this template can be deleted. 9 | 10 | #### Overview of the Issue 11 | 12 | A paragraph or two about the issue you're experiencing. 13 | 14 | #### Reproduction Steps 15 | 16 | Steps to reproduce this issue 17 | 18 | ### Plugin and Packer version 19 | 20 | From `packer version` 21 | 22 | ### Simplified Packer Buildfile 23 | 24 | If the file is longer than a few dozen lines, please include the URL to the 25 | [gist](https://gist.github.com/) of the log or use the [Github detailed 26 | format](https://gist.github.com/ericclemmons/b146fe5da72ca1f706b2ef72a20ac39d) 27 | instead of posting it directly in the issue. 28 | 29 | ### Operating system and Environment details 30 | 31 | OS, Architecture, and any other information you can provide about the 32 | environment. 33 | 34 | ### Log Fragments and crash.log files 35 | 36 | Include appropriate log fragments. If the log is longer than a few dozen lines, 37 | please include the URL to the [gist](https://gist.github.com/) of the log or 38 | use the [Github detailed format](https://gist.github.com/ericclemmons/b146fe5da72ca1f706b2ef72a20ac39d) instead of posting it directly in the issue. 39 | 40 | Set the env var `PACKER_LOG=1` for maximum log detail. 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_requests.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: If you have something you think this Packer plugin could improve or add support for. 4 | labels: enhancement 5 | --- 6 | 7 | Please search the existing issues for relevant feature requests, and use the 8 | reaction feature 9 | (https://blog.github.com/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/) 10 | to add upvotes to pre-existing requests. 11 | 12 | #### Community Note 13 | 14 | Please vote on this issue by adding a 👍 [reaction](https://blog.github.com/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/) to the original issue to help the community and maintainers prioritize this request. 15 | Please do not leave "+1" or "me too" comments, they generate extra noise for issue followers and do not help prioritize the request. 16 | If you are interested in working on this issue or have submitted a pull request, please leave a comment. 17 | 18 | #### Description 19 | 20 | A written overview of the feature. 21 | 22 | #### Use Case(s) 23 | 24 | Any relevant use-cases that you see. 25 | 26 | #### Potential configuration 27 | 28 | ``` 29 | ``` 30 | 31 | #### Potential References 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: If you have a question, please check out our other community resources instead of opening an issue. 4 | labels: question 5 | --- 6 | 7 | Issues on GitHub are intended to be related to bugs or feature requests, so we 8 | recommend using our other community resources instead of asking here if you 9 | have a question. 10 | 11 | - Packer Guides: https://developer.hashicorp.com/packer/guides 12 | - Packer Community Tools: https://developer.hashicorp.com/packer/docs/community-tools enumerates 13 | vetted community resources like examples and useful tools 14 | - Any other questions can be sent to the Packer section of the HashiCorp 15 | forum: https://discuss.hashicorp.com/c/packer 16 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **DELETE THIS PART BEFORE SUBMITTING** 2 | 3 | In order to have a good experience with our community, we recommend that you 4 | read the contributing guidelines for making a PR, and understand the lifecycle 5 | of a Packer Plugin PR: 6 | 7 | https://github.com/hashicorp/packer-plugin-proxmox/blob/main/.github/CONTRIBUTING.md#opening-an-pull-request 8 | 9 | ---- 10 | 11 | ### Description 12 | What code changed, and why? 13 | 14 | 15 | ### Resolved Issues 16 | If your PR resolves any open issue(s), please indicate them like this so they will be closed when your PR is merged: 17 | 18 | Closes #xxx 19 | Closes #xxx 20 | 21 | 22 | ### Rollback Plan 23 | 24 | If a change needs to be reverted, we will roll out an update to the code within 7 days. 25 | 26 | ### Changes to Security Controls 27 | 28 | Are there any changes to security controls (access controls, encryption, logging) in this pull request? If so, explain. 29 | 30 | -------------------------------------------------------------------------------- /.github/actions/build-and-persist-plugin-binary/action.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | name: build-and-persist-plugin-binary 5 | inputs: 6 | GOOS: 7 | required: true 8 | GOARCH: 9 | required: true 10 | runs: 11 | using: composite 12 | steps: 13 | - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 14 | - run: "GOOS=${{ inputs.GOOS }} GOARCH=${{ inputs.GOARCH }} go build -o ./pkg/packer_plugin_proxmox_${{ inputs.GOOS }}_${{ inputs.GOARCH }} ." 15 | shell: bash 16 | - run: zip ./pkg/packer_plugin_proxmox_${{ inputs.GOOS }}_${{ inputs.GOARCH }}.zip ./pkg/packer_plugin_proxmox_${{ inputs.GOOS }}_${{ inputs.GOARCH }} 17 | shell: bash 18 | - run: rm ./pkg/packer_plugin_proxmox_${{ inputs.GOOS }}_${{ inputs.GOARCH }} 19 | shell: bash 20 | - uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 21 | with: 22 | name: "packer_plugin_proxmox_${{ inputs.GOOS }}_${{ inputs.GOARCH }}.zip" 23 | path: "pkg/packer_plugin_proxmox_${{ inputs.GOOS }}_${{ inputs.GOARCH }}.zip" 24 | retention-days: 30 25 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | version: 2 5 | updates: 6 | - package-ecosystem: "gomod" # See documentation for possible values 7 | directory: "/" # Location of package manifests 8 | schedule: 9 | interval: "daily" 10 | allow: 11 | - dependency-name: "github.com/hashicorp/packer-plugin-sdk" 12 | 13 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | changelog: 5 | exclude: 6 | labels: 7 | - ignore-for-release 8 | categories: 9 | - title: Breaking Changes 🛠 10 | labels: 11 | - breaking-change 12 | - title: Exciting New Features 🎉 13 | labels: 14 | - enhancement 15 | - title: Bug fixes🧑‍🔧 🐞 16 | labels: 17 | - bug 18 | - title: Doc improvements 📚 19 | labels: 20 | - docs 21 | - documentation 22 | - title: Other Changes 23 | labels: 24 | - "*" 25 | 26 | -------------------------------------------------------------------------------- /.github/workflows/build_plugin_binaries.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | name: hashicorp/packer-plugin-proxmox/build_plugin_binaries 5 | permissions: 6 | contents: read 7 | on: 8 | push: 9 | branches: 10 | - main 11 | jobs: 12 | build_darwin: 13 | defaults: 14 | run: 15 | working-directory: ~/go/src/github.com/hashicorp/packer-plugin-proxmox 16 | runs-on: ubuntu-latest 17 | container: 18 | image: docker.mirror.hashicorp.services/cimg/go:1.21 19 | steps: 20 | - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 21 | - uses: "./.github/actions/build-and-persist-plugin-binary" 22 | with: 23 | GOOS: darwin 24 | GOARCH: amd64 25 | - uses: "./.github/actions/build-and-persist-plugin-binary" 26 | with: 27 | GOOS: darwin 28 | GOARCH: arm64 29 | build_freebsd: 30 | defaults: 31 | run: 32 | working-directory: ~/go/src/github.com/hashicorp/packer-plugin-proxmox 33 | runs-on: ubuntu-latest 34 | container: 35 | image: docker.mirror.hashicorp.services/cimg/go:1.21 36 | steps: 37 | - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 38 | - uses: "./.github/actions/build-and-persist-plugin-binary" 39 | with: 40 | GOOS: freebsd 41 | GOARCH: 386 42 | - uses: "./.github/actions/build-and-persist-plugin-binary" 43 | with: 44 | GOOS: freebsd 45 | GOARCH: amd64 46 | - uses: "./.github/actions/build-and-persist-plugin-binary" 47 | with: 48 | GOOS: freebsd 49 | GOARCH: arm 50 | build_linux: 51 | defaults: 52 | run: 53 | working-directory: ~/go/src/github.com/hashicorp/packer-plugin-proxmox 54 | runs-on: ubuntu-latest 55 | container: 56 | image: docker.mirror.hashicorp.services/cimg/go:1.21 57 | steps: 58 | - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 59 | - uses: "./.github/actions/build-and-persist-plugin-binary" 60 | with: 61 | GOOS: linux 62 | GOARCH: 386 63 | - uses: "./.github/actions/build-and-persist-plugin-binary" 64 | with: 65 | GOOS: linux 66 | GOARCH: amd64 67 | - uses: "./.github/actions/build-and-persist-plugin-binary" 68 | with: 69 | GOOS: linux 70 | GOARCH: arm 71 | - uses: "./.github/actions/build-and-persist-plugin-binary" 72 | with: 73 | GOOS: linux 74 | GOARCH: arm64 75 | build_netbsd: 76 | defaults: 77 | run: 78 | working-directory: ~/go/src/github.com/hashicorp/packer-plugin-proxmox 79 | runs-on: ubuntu-latest 80 | container: 81 | image: docker.mirror.hashicorp.services/cimg/go:1.21 82 | steps: 83 | - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 84 | - uses: "./.github/actions/build-and-persist-plugin-binary" 85 | with: 86 | GOOS: netbsd 87 | GOARCH: 386 88 | - uses: "./.github/actions/build-and-persist-plugin-binary" 89 | with: 90 | GOOS: netbsd 91 | GOARCH: amd64 92 | - uses: "./.github/actions/build-and-persist-plugin-binary" 93 | with: 94 | GOOS: netbsd 95 | GOARCH: arm 96 | build_openbsd: 97 | defaults: 98 | run: 99 | working-directory: ~/go/src/github.com/hashicorp/packer-plugin-proxmox 100 | runs-on: ubuntu-latest 101 | container: 102 | image: docker.mirror.hashicorp.services/cimg/go:1.21 103 | steps: 104 | - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 105 | - uses: "./.github/actions/build-and-persist-plugin-binary" 106 | with: 107 | GOOS: openbsd 108 | GOARCH: 386 109 | - uses: "./.github/actions/build-and-persist-plugin-binary" 110 | with: 111 | GOOS: openbsd 112 | GOARCH: amd64 113 | - uses: "./.github/actions/build-and-persist-plugin-binary" 114 | with: 115 | GOOS: openbsd 116 | GOARCH: arm 117 | build_solaris: 118 | defaults: 119 | run: 120 | working-directory: ~/go/src/github.com/hashicorp/packer-plugin-proxmox 121 | runs-on: ubuntu-latest 122 | container: 123 | image: docker.mirror.hashicorp.services/cimg/go:1.21 124 | steps: 125 | - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 126 | - uses: "./.github/actions/build-and-persist-plugin-binary" 127 | with: 128 | GOOS: solaris 129 | GOARCH: amd64 130 | build_windows: 131 | defaults: 132 | run: 133 | working-directory: ~/go/src/github.com/hashicorp/packer-plugin-proxmox 134 | runs-on: ubuntu-latest 135 | container: 136 | image: docker.mirror.hashicorp.services/cimg/go:1.21 137 | steps: 138 | - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 139 | - uses: "./.github/actions/build-and-persist-plugin-binary" 140 | with: 141 | GOOS: windows 142 | GOARCH: 386 143 | - uses: "./.github/actions/build-and-persist-plugin-binary" 144 | with: 145 | GOOS: windows 146 | GOARCH: amd64 147 | -------------------------------------------------------------------------------- /.github/workflows/go-test-darwin.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) HashiCorp, Inc. 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | # This GitHub action runs Packer go tests across 6 | # MacOS runners. 7 | # 8 | 9 | name: "Go Test MacOS" 10 | 11 | on: 12 | push: 13 | branches: 14 | - 'main' 15 | pull_request: 16 | 17 | permissions: 18 | contents: read 19 | 20 | jobs: 21 | get-go-version: 22 | runs-on: ubuntu-latest 23 | outputs: 24 | go-version: ${{ steps.get-go-version.outputs.go-version }} 25 | steps: 26 | - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 27 | - name: 'Determine Go version' 28 | id: get-go-version 29 | run: | 30 | echo "Found Go $(cat .go-version)" 31 | echo "go-version=$(cat .go-version)" >> $GITHUB_OUTPUT 32 | darwin-go-tests: 33 | needs: 34 | - get-go-version 35 | runs-on: macos-latest 36 | name: Darwin Go tests 37 | steps: 38 | - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 39 | - uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 40 | with: 41 | go-version: ${{ needs.get-go-version.outputs.go-version }} 42 | - run: | 43 | echo "Testing with Go ${{ needs.get-go-version.outputs.go-version }}" 44 | go test -race -count 1 ./... -timeout=3m 45 | 46 | 47 | -------------------------------------------------------------------------------- /.github/workflows/go-test-linux.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | # 5 | # This GitHub action runs Packer go tests across 6 | # Linux runners. 7 | # 8 | 9 | name: "Go Test Linux" 10 | 11 | on: 12 | push: 13 | branches: 14 | - 'main' 15 | pull_request: 16 | 17 | permissions: 18 | contents: read 19 | 20 | jobs: 21 | get-go-version: 22 | runs-on: ubuntu-latest 23 | outputs: 24 | go-version: ${{ steps.get-go-version.outputs.go-version }} 25 | steps: 26 | - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 27 | - name: 'Determine Go version' 28 | id: get-go-version 29 | run: | 30 | echo "Found Go $(cat .go-version)" 31 | echo "go-version=$(cat .go-version)" >> $GITHUB_OUTPUT 32 | linux-go-tests: 33 | needs: 34 | - get-go-version 35 | runs-on: ubuntu-latest 36 | name: Linux Go tests 37 | steps: 38 | - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 39 | - uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 40 | with: 41 | go-version: ${{ needs.get-go-version.outputs.go-version }} 42 | - run: | 43 | echo "Testing with Go ${{ needs.get-go-version.outputs.go-version }}" 44 | go test -race -count 1 ./... -timeout=3m 45 | -------------------------------------------------------------------------------- /.github/workflows/go-test-windows.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | # 5 | # This GitHub action runs Packer go tests across 6 | # Windows runners. 7 | # 8 | 9 | name: "Go Test Windows" 10 | 11 | on: 12 | push: 13 | branches: 14 | - 'main' 15 | pull_request: 16 | 17 | permissions: 18 | contents: read 19 | 20 | jobs: 21 | get-go-version: 22 | runs-on: ubuntu-latest 23 | outputs: 24 | go-version: ${{ steps.get-go-version.outputs.go-version }} 25 | steps: 26 | - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 27 | - name: 'Determine Go version' 28 | id: get-go-version 29 | run: | 30 | echo "Found Go $(cat .go-version)" 31 | echo "go-version=$(cat .go-version)" >> $GITHUB_OUTPUT 32 | windows-go-tests: 33 | needs: 34 | - get-go-version 35 | runs-on: windows-latest 36 | name: Windows Go tests 37 | steps: 38 | - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 39 | - uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 40 | with: 41 | go-version: ${{ needs.get-go-version.outputs.go-version }} 42 | - run: | 43 | echo "Testing with Go ${{ needs.get-go-version.outputs.go-version }}" 44 | go test -race -count 1 ./... -timeout=3m 45 | 46 | -------------------------------------------------------------------------------- /.github/workflows/go-validate.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | # 5 | # This GitHub action runs basic linting checks for Packer. 6 | # 7 | 8 | name: "Go Validate" 9 | 10 | on: 11 | push: 12 | branches: 13 | - 'main' 14 | pull_request: 15 | 16 | permissions: 17 | contents: read 18 | 19 | jobs: 20 | get-go-version: 21 | runs-on: ubuntu-latest 22 | outputs: 23 | go-version: ${{ steps.get-go-version.outputs.go-version }} 24 | steps: 25 | - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 26 | - name: 'Determine Go version' 27 | id: get-go-version 28 | run: | 29 | echo "Found Go $(cat .go-version)" 30 | echo "go-version=$(cat .go-version)" >> $GITHUB_OUTPUT 31 | check-mod-tidy: 32 | needs: 33 | - get-go-version 34 | runs-on: ubuntu-latest 35 | name: Go Mod Tidy 36 | steps: 37 | - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 38 | - uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 39 | with: 40 | go-version: ${{ needs.get-go-version.outputs.go-version }} 41 | - run: go mod tidy 42 | check-lint: 43 | needs: 44 | - get-go-version 45 | runs-on: ubuntu-latest 46 | name: Lint check 47 | steps: 48 | - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 49 | - uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 50 | with: 51 | go-version: ${{ needs.get-go-version.outputs.go-version }} 52 | - uses: golangci/golangci-lint-action@82d40c283aeb1f2b6595839195e95c2d6a49081b # v5.0.0 53 | with: 54 | version: v1.60.1 55 | only-new-issues: true 56 | check-fmt: 57 | needs: 58 | - get-go-version 59 | runs-on: ubuntu-latest 60 | name: Gofmt check 61 | steps: 62 | - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 63 | - uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 64 | with: 65 | go-version: ${{ needs.get-go-version.outputs.go-version }} 66 | - run: | 67 | go fmt ./... 68 | echo "==> Checking that code complies with go fmt requirements..." 69 | git diff --exit-code; if [ $$? -eq 1 ]; then \ 70 | echo "Found files that are not fmt'ed."; \ 71 | echo "You can use the command: \`go fmt ./...\` to reformat code."; \ 72 | exit 1; \ 73 | fi 74 | check-generate: 75 | needs: 76 | - get-go-version 77 | runs-on: ubuntu-latest 78 | name: Generate check 79 | steps: 80 | - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 81 | - uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 82 | with: 83 | go-version: ${{ needs.get-go-version.outputs.go-version }} 84 | - run: | 85 | export PATH=$PATH:$(go env GOPATH)/bin 86 | make generate 87 | uncommitted="$(git status -s)" 88 | if [[ -z "$uncommitted" ]]; then 89 | echo "OK" 90 | else 91 | echo "Docs have been updated, but the compiled docs have not been committed." 92 | echo "Run 'make generate', and commit the result to resolve this error." 93 | echo "Generated but uncommitted files:" 94 | echo "$uncommitted" 95 | exit 1 96 | fi 97 | -------------------------------------------------------------------------------- /.github/workflows/notify-integration-release-via-manual.yaml: -------------------------------------------------------------------------------- 1 | name: Notify Integration Release (Manual) 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | version: 6 | description: "The release version (semver)" 7 | default: 0.0.1 8 | required: false 9 | branch: 10 | description: "A branch or SHA" 11 | default: 'main' 12 | required: false 13 | jobs: 14 | strip-version: 15 | runs-on: ubuntu-latest 16 | outputs: 17 | packer-version: ${{ steps.strip.outputs.packer-version }} 18 | steps: 19 | - name: Strip leading v from version tag 20 | id: strip 21 | env: 22 | REF: ${{ github.event.inputs.version }} 23 | run: | 24 | echo "packer-version=$(echo "$REF" | sed -E 's/v?([0-9]+\.[0-9]+\.[0-9]+)/\1/')" >> "$GITHUB_OUTPUT" 25 | notify-release: 26 | needs: 27 | - strip-version 28 | runs-on: ubuntu-latest 29 | steps: 30 | - name: Checkout this repo 31 | uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 32 | with: 33 | ref: ${{ github.event.inputs.branch }} 34 | # Ensure that Docs are Compiled 35 | - uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 36 | - shell: bash 37 | run: make generate 38 | - shell: bash 39 | run: | 40 | uncommitted="$(git status -s)" 41 | if [[ -z "$uncommitted" ]]; then 42 | echo "OK" 43 | else 44 | echo "Docs have been updated, but the compiled docs have not been committed." 45 | echo "Run 'make generate', and commit the result to resolve this error." 46 | echo "Generated but uncommitted files:" 47 | echo "$uncommitted" 48 | exit 1 49 | fi 50 | # Perform the Release 51 | - name: Checkout integration-release-action 52 | uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 53 | with: 54 | repository: hashicorp/integration-release-action 55 | path: ./integration-release-action 56 | - name: Notify Release 57 | uses: ./integration-release-action 58 | with: 59 | integration_identifier: "packer/hashicorp/proxmox" 60 | release_version: ${{ needs.strip-version.outputs.packer-version }} 61 | release_sha: ${{ github.event.inputs.branch }} 62 | github_token: ${{ secrets.GITHUB_TOKEN }} 63 | -------------------------------------------------------------------------------- /.github/workflows/notify-integration-release-via-tag.yaml: -------------------------------------------------------------------------------- 1 | name: Notify Integration Release (Tag) 2 | on: 3 | push: 4 | tags: 5 | - '*.*.*' # Proper releases 6 | jobs: 7 | strip-version: 8 | runs-on: ubuntu-latest 9 | outputs: 10 | packer-version: ${{ steps.strip.outputs.packer-version }} 11 | steps: 12 | - name: Strip leading v from version tag 13 | id: strip 14 | env: 15 | REF: ${{ github.ref_name }} 16 | run: | 17 | echo "packer-version=$(echo "$REF" | sed -E 's/v?([0-9]+\.[0-9]+\.[0-9]+)/\1/')" >> "$GITHUB_OUTPUT" 18 | notify-release: 19 | needs: 20 | - strip-version 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Checkout this repo 24 | uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 25 | with: 26 | ref: ${{ github.ref }} 27 | # Ensure that Docs are Compiled 28 | - uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 29 | - shell: bash 30 | run: make generate 31 | - shell: bash 32 | run: | 33 | uncommitted="$(git status -s)" 34 | if [[ -z "$uncommitted" ]]; then 35 | echo "OK" 36 | else 37 | echo "Docs have been updated, but the compiled docs have not been committed." 38 | echo "Run 'make generate', and commit the result to resolve this error." 39 | echo "Generated but uncommitted files:" 40 | echo "$uncommitted" 41 | exit 1 42 | fi 43 | # Perform the Release 44 | - name: Checkout integration-release-action 45 | uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 46 | with: 47 | repository: hashicorp/integration-release-action 48 | path: ./integration-release-action 49 | - name: Notify Release 50 | uses: ./integration-release-action 51 | with: 52 | integration_identifier: "packer/hashicorp/proxmox" 53 | release_version: ${{ needs.strip-version.outputs.packer-version }} 54 | release_sha: ${{ github.ref }} 55 | github_token: ${{ secrets.GITHUB_TOKEN }} 56 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | # This GitHub action can publish assets for release when a tag is created. 5 | # Currently its setup to run on any tag that matches the pattern "v*" (ie. v0.1.0). 6 | # 7 | # This uses an action (hashicorp/ghaction-import-gpg) that assumes you set your 8 | # private key in the `GPG_PRIVATE_KEY` secret and passphrase in the `GPG_PASSPHRASE` 9 | # secret. If you would rather own your own GPG handling, please fork this action 10 | # or use an alternative one for key handling. 11 | # 12 | # You will need to pass the `--batch` flag to `gpg` in your signing step 13 | # in `goreleaser` to indicate this is being used in a non-interactive mode. 14 | # 15 | name: release 16 | on: 17 | push: 18 | tags: 19 | - 'v*' 20 | permissions: 21 | contents: write 22 | packages: read 23 | jobs: 24 | get-go-version: 25 | runs-on: ubuntu-latest 26 | outputs: 27 | go-version: ${{ steps.get-go-version.outputs.go-version }} 28 | steps: 29 | - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 30 | - name: 'Determine Go version' 31 | id: get-go-version 32 | run: | 33 | echo "Found Go $(cat .go-version)" 34 | echo "go-version=$(cat .go-version)" >> $GITHUB_OUTPUT 35 | goreleaser: 36 | needs: 37 | - get-go-version 38 | runs-on: ubuntu-latest 39 | steps: 40 | - name: Checkout 41 | uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 42 | - name: Unshallow 43 | run: git fetch --prune --unshallow 44 | - name: Set up Go 45 | uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 46 | with: 47 | go-version: ${{ needs.get-go-version.outputs.go-version }} 48 | - name: Describe plugin 49 | id: plugin_describe 50 | run: echo "api_version=$(go run . describe | jq -r '.api_version')" >> "$GITHUB_OUTPUT" 51 | - name: Install signore 52 | uses: hashicorp/setup-signore-package@v1 53 | - name: Run GoReleaser 54 | uses: goreleaser/goreleaser-action@7ec5c2b0c6cdda6e8bbb49444bc797dd33d74dd8 # v5.0.0 55 | with: 56 | version: latest 57 | args: release --clean --timeout 120m 58 | env: 59 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 60 | API_VERSION: ${{ steps.plugin_describe.outputs.api_version }} 61 | SIGNORE_CLIENT_ID: ${{ secrets.SIGNORE_CLIENT_ID }} 62 | SIGNORE_CLIENT_SECRET: ${{ secrets.SIGNORE_CLIENT_SECRET }} 63 | SIGNORE_SIGNER: ${{ secrets.SIGNORE_SIGNER }} 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | main 2 | dist/* 3 | packer-plugin-proxmox 4 | crash.log 5 | -------------------------------------------------------------------------------- /.go-version: -------------------------------------------------------------------------------- 1 | 1.21.13 2 | 3 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | issues: 5 | # List of regexps of issue texts to exclude, empty list by default. 6 | # But independently from this option we use default exclude patterns, 7 | # it can be disabled by `exclude-use-default: false`. To list all 8 | # excluded by default patterns execute `golangci-lint run --help` 9 | 10 | exclude-rules: 11 | # Exclude gosimple bool check 12 | - linters: 13 | - gosimple 14 | text: "S(1002|1008|1021)" 15 | # Exclude failing staticchecks for now 16 | - linters: 17 | - staticcheck 18 | text: "SA(1006|1019|4006|4010|4017|5007|6005|9004):" 19 | # Exclude lll issues for long lines with go:generate 20 | - linters: 21 | - lll 22 | source: "^//go:generate " 23 | - linters: 24 | - errcheck 25 | path: ".*_test.go" 26 | 27 | # Maximum issues count per one linter. Set to 0 to disable. Default is 50. 28 | max-issues-per-linter: 0 29 | 30 | # Maximum count of issues with the same text. Set to 0 to disable. Default is 3. 31 | max-same-issues: 0 32 | 33 | linters: 34 | disable-all: true 35 | enable: 36 | - errcheck 37 | - goimports 38 | - gosimple 39 | - govet 40 | - ineffassign 41 | - staticcheck 42 | - unconvert 43 | - unused 44 | fast: true 45 | 46 | # options for analysis running 47 | run: 48 | # default concurrency is a available CPU number 49 | concurrency: 4 50 | 51 | # timeout for analysis, e.g. 30s, 5m, default is 1m 52 | timeout: 10m 53 | 54 | # exit code when at least one issue was found, default is 1 55 | issues-exit-code: 1 56 | 57 | # include test files or not, default is true 58 | tests: true 59 | 60 | # list of build tags, all linters use it. Default is empty list. 61 | #build-tags: 62 | # - mytag 63 | 64 | # which dirs to skip: issues from them won't be reported; 65 | # can use regexp here: generated.*, regexp is applied on full path; 66 | # default value is empty list, but default dirs are skipped independently 67 | # from this option's value (see skip-dirs-use-default). 68 | #skip-dirs: 69 | # - src/external_libs 70 | # - autogenerated_by_my_lib 71 | 72 | # default is true. Enables skipping of directories: 73 | # vendor$, third_party$, testdata$, examples$, Godeps$, builtin$ 74 | skip-dirs-use-default: true 75 | 76 | # which files to skip: they will be analyzed, but issues from them 77 | # won't be reported. Default value is empty list, but there is 78 | # no need to include all autogenerated files, we confidently recognize 79 | # autogenerated files. If it's not please let us know. 80 | exclude-files: 81 | - ".*\\.hcl2spec\\.go$" 82 | # - lib/bad.go 83 | 84 | # by default isn't set. If set we pass it to "go list -mod={option}". From "go help modules": 85 | # If invoked with -mod=readonly, the go command is disallowed from the implicit 86 | # automatic updating of go.mod described above. Instead, it fails when any changes 87 | # to go.mod are needed. This setting is most useful to check that go.mod does 88 | # not need updates, such as in a continuous integration and testing system. 89 | # If invoked with -mod=vendor, the go command assumes that the vendor 90 | # directory holds the correct copies of dependencies and ignores 91 | # the dependency descriptions in go.mod. 92 | # modules-download-mode: vendor 93 | 94 | 95 | # output configuration options 96 | output: 97 | # colored-line-number|line-number|json|tab|checkstyle|code-climate, default is "colored-line-number" 98 | formats: colored-line-number 99 | 100 | # print lines of code with issue, default is true 101 | print-issued-lines: true 102 | 103 | # print linter name in the end of issue text, default is true 104 | print-linter-name: true 105 | 106 | # make issues output unique by line, default is true 107 | uniq-by-line: true 108 | 109 | 110 | # all available settings of specific linters 111 | linters-settings: 112 | errcheck: 113 | # report about not checking of errors in type assetions: `a := b.(MyStruct)`; 114 | # default is false: such cases aren't reported by default. 115 | check-type-assertions: false 116 | 117 | # report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`; 118 | # default is false: such cases aren't reported by default. 119 | check-blank: false 120 | 121 | # [deprecated] comma-separated list of pairs of the form pkg:regex 122 | # the regex is used to ignore names within pkg. (default "fmt:.*"). 123 | # see https://github.com/kisielk/errcheck#the-deprecated-method for details 124 | exclude-functions: fmt:.*,io/ioutil:^Read.*,io:Close 125 | 126 | # path to a file containing a list of functions to exclude from checking 127 | # see https://github.com/kisielk/errcheck#excluding-functions for details 128 | #exclude: /path/to/file.txt 129 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | # This is an example goreleaser.yaml file with some defaults. 5 | # Make sure to check the documentation at http://goreleaser.com 6 | env: 7 | - CGO_ENABLED=0 8 | before: 9 | hooks: 10 | # We strongly recommend running tests to catch any regression before release. 11 | # Even though, this an optional step. 12 | - go test ./... 13 | # Check plugin compatibility with required version of the Packer SDK 14 | - make plugin-check 15 | # Copy LICENSE file for inclusion in zip archive 16 | - cp LICENSE LICENSE.txt 17 | builds: 18 | # A separated build to run the packer-plugins-check only once for a linux_amd64 binary 19 | - 20 | id: plugin-check 21 | mod_timestamp: '{{ .CommitTimestamp }}' 22 | flags: 23 | - -trimpath #removes all file system paths from the compiled executable 24 | ldflags: 25 | - '-s -w -X {{ .ModulePath }}/version.Version={{.Version}} -X {{ .ModulePath }}/version.VersionPrerelease= ' 26 | goos: 27 | - linux 28 | goarch: 29 | - amd64 30 | binary: '{{ .ProjectName }}_v{{ .Version }}_{{ .Env.API_VERSION }}_{{ .Os }}_{{ .Arch }}' 31 | - 32 | id: linux-builds 33 | mod_timestamp: '{{ .CommitTimestamp }}' 34 | flags: 35 | - -trimpath #removes all file system paths from the compiled executable 36 | ldflags: 37 | - '-s -w -X {{ .ModulePath }}/version.Version={{.Version}} -X {{ .ModulePath }}/version.VersionPrerelease= ' 38 | goos: 39 | - linux 40 | goarch: 41 | - amd64 42 | - '386' 43 | - arm 44 | - arm64 45 | ignore: 46 | - goos: linux 47 | goarch: amd64 48 | binary: '{{ .ProjectName }}_v{{ .Version }}_{{ .Env.API_VERSION }}_{{ .Os }}_{{ .Arch }}' 49 | - 50 | id: darwin-builds 51 | mod_timestamp: '{{ .CommitTimestamp }}' 52 | flags: 53 | - -trimpath #removes all file system paths from the compiled executable 54 | ldflags: 55 | - '-s -w -X {{ .ModulePath }}/version.Version={{.Version}} -X {{ .ModulePath }}/version.VersionPrerelease= ' 56 | goos: 57 | - darwin 58 | goarch: 59 | - amd64 60 | - arm64 61 | binary: '{{ .ProjectName }}_v{{ .Version }}_{{ .Env.API_VERSION }}_{{ .Os }}_{{ .Arch }}' 62 | - 63 | id: other-builds 64 | mod_timestamp: '{{ .CommitTimestamp }}' 65 | flags: 66 | - -trimpath #removes all file system paths from the compiled executable 67 | ldflags: 68 | - '-s -w -X {{ .ModulePath }}/version.Version={{.Version}} -X {{ .ModulePath }}/version.VersionPrerelease= ' 69 | goos: 70 | - netbsd 71 | - openbsd 72 | - freebsd 73 | - windows 74 | - solaris 75 | goarch: 76 | - amd64 77 | - '386' 78 | - arm 79 | ignore: 80 | - goos: windows 81 | goarch: arm 82 | - goos: solaris 83 | goarch: arm 84 | - goos: solaris 85 | goarch: '386' 86 | binary: '{{ .ProjectName }}_v{{ .Version }}_{{ .Env.API_VERSION }}_{{ .Os }}_{{ .Arch }}' 87 | archives: 88 | - format: zip 89 | files: 90 | - "LICENSE.txt" 91 | 92 | name_template: '{{ .ProjectName }}_v{{ .Version }}_{{ .Env.API_VERSION }}_{{ .Os }}_{{ .Arch }}' 93 | checksum: 94 | name_template: '{{ .ProjectName }}_v{{ .Version }}_SHA256SUMS' 95 | algorithm: sha256 96 | signs: 97 | - cmd: signore 98 | args: ["sign", "--dearmor", "--file", "${artifact}", "--out", "${signature}"] 99 | artifacts: checksum 100 | signature: ${artifact}.sig 101 | 102 | changelog: 103 | use: github-native 104 | -------------------------------------------------------------------------------- /.web-docs/README.md: -------------------------------------------------------------------------------- 1 | The Proxmox Packer builder is able to create [Proxmox](https://www.proxmox.com/en/proxmox-ve) virtual machines and store them as new Proxmox Virtual Machine images. 2 | 3 | ### Installation 4 | 5 | To install this plugin add this code into your Packer configuration and run [packer init](/packer/docs/commands/init) 6 | 7 | ```hcl 8 | packer { 9 | required_plugins { 10 | name = { 11 | version = "~> 1" 12 | source = "github.com/hashicorp/proxmox" 13 | } 14 | } 15 | } 16 | ``` 17 | Alternatively, you can use `packer plugins install` to manage installation of this plugin. 18 | 19 | ```sh 20 | packer plugins install github.com/hashicorp/proxmox 21 | ``` 22 | 23 | ### Components 24 | 25 | Packer is able to target both ISO and existing Cloud-Init images. 26 | 27 | #### Builders 28 | 29 | - [proxmox-clone](/packer/integrations/hashicorp/proxmox/latest/components/builder/clone) - The proxmox image 30 | builder is able to create new images for use with Proxmox VE. The builder takes a cloud-init enabled virtual machine 31 | template name, runs any provisioning necessary on the image after 32 | launching it, then creates a virtual machine template. 33 | - [proxmox-iso](/packer/integrations/hashicorp/proxmox/latest/components/builder/iso) - The proxmox ISO 34 | builder is able to create new images for use with Proxmox VE. The builder 35 | takes an ISO source, runs any provisioning necessary on the image after 36 | launching it, then creates a virtual machine template. 37 | 38 | -------------------------------------------------------------------------------- /.web-docs/metadata.hcl: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | # For full specification on the configuration of this file visit: 5 | # https://github.com/hashicorp/integration-template#metadata-configuration 6 | integration { 7 | name = "Proxmox" 8 | description = "The Proxmox Packer builder is able to create Proxmox virtual machines and store them as new Proxmox Virtual Machine images." 9 | identifier = "packer/hashicorp/proxmox" 10 | component { 11 | type = "builder" 12 | name = "Proxmox Clone" 13 | slug = "clone" 14 | } 15 | component { 16 | type = "builder" 17 | name = "Proxmox ISO" 18 | slug = "iso" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.web-docs/scripts/compile-to-webdocs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright (c) HashiCorp, Inc. 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | 6 | # Converts the folder name that the component documentation file 7 | # is stored in into the integration slug of the component. 8 | componentTypeFromFolderName() { 9 | if [[ "$1" = "builders" ]]; then 10 | echo "builder" 11 | elif [[ "$1" = "provisioners" ]]; then 12 | echo "provisioner" 13 | elif [[ "$1" = "post-processors" ]]; then 14 | echo "post-processor" 15 | elif [[ "$1" = "datasources" ]]; then 16 | echo "data-source" 17 | else 18 | echo "" 19 | fi 20 | } 21 | 22 | # $1: The content to adjust links 23 | # $2: The organization of the integration 24 | rewriteLinks() { 25 | local result="$1" 26 | local organization="$2" 27 | 28 | urlSegment="([^/]+)" 29 | urlAnchor="(#[^/]+)" 30 | 31 | # Rewrite Component Index Page links to the Integration root page. 32 | # 33 | # (\1) (\2) (\3) 34 | # /packer/plugins/datasources/amazon#anchor-tag--> 35 | # /packer/integrations/hashicorp/amazon#anchor-tag 36 | local find="\(\/packer\/plugins\/$urlSegment\/$urlSegment$urlAnchor?\)" 37 | local replace="\(\/packer\/integrations\/$organization\/\2\3\)" 38 | result="$(echo "$result" | sed -E "s/$find/$replace/g")" 39 | 40 | 41 | # Rewrite Component links to the Integration component page 42 | # 43 | # (\1) (\2) (\3) (\4) 44 | # /packer/plugins/datasources/amazon/parameterstore#anchor-tag --> 45 | # /packer/integrations/{organization}/amazon/latest/components/datasources/parameterstore 46 | local find="\(\/packer\/plugins\/$urlSegment\/$urlSegment\/$urlSegment$urlAnchor?\)" 47 | local replace="\(\/packer\/integrations\/$organization\/\2\/latest\/components\/\1\/\3\4\)" 48 | result="$(echo "$result" | sed -E "s/$find/$replace/g")" 49 | 50 | # Rewrite the Component URL segment from the Packer Plugin format 51 | # to the Integrations format 52 | result="$(echo "$result" \ 53 | | sed "s/\/datasources\//\/data-source\//g" \ 54 | | sed "s/\/builders\//\/builder\//g" \ 55 | | sed "s/\/post-processors\//\/post-processor\//g" \ 56 | | sed "s/\/provisioners\//\/provisioner\//g" \ 57 | )" 58 | 59 | echo "$result" 60 | } 61 | 62 | # $1: Docs Dir 63 | # $2: Web Docs Dir 64 | # $3: Component File 65 | # $4: The org of the integration 66 | processComponentFile() { 67 | local docsDir="$1" 68 | local webDocsDir="$2" 69 | local componentFile="$3" 70 | 71 | local escapedDocsDir="$(echo "$docsDir" | sed 's/\//\\\//g' | sed 's/\./\\\./g')" 72 | local componentTypeAndSlug="$(echo "$componentFile" | sed "s/$escapedDocsDir\///g" | sed 's/\.mdx//g')" 73 | 74 | # Parse out the Component Slug & Component Type 75 | local componentSlug="$(echo "$componentTypeAndSlug" | cut -d'/' -f 2)" 76 | local componentType="$(componentTypeFromFolderName "$(echo "$componentTypeAndSlug" | cut -d'/' -f 1)")" 77 | if [[ "$componentType" = "" ]]; then 78 | echo "Failed to process '$componentFile', unexpected folder name." 79 | echo "Documentation for components must be stored in one of:" 80 | echo "builders, provisioners, post-processors, datasources" 81 | exit 1 82 | fi 83 | 84 | 85 | # Calculate the location of where this file will ultimately go 86 | local webDocsFolder="$webDocsDir/components/$componentType/$componentSlug" 87 | mkdir -p "$webDocsFolder" 88 | local webDocsFile="$webDocsFolder/README.md" 89 | local webDocsFileTmp="$webDocsFolder/README.md.tmp" 90 | 91 | # Copy over the file to its webDocsFile location 92 | cp "$componentFile" "$webDocsFile" 93 | 94 | # Remove the Header 95 | local lastMetadataLine="$(grep -n -m 2 '^\-\-\-' "$componentFile" | tail -n1 | cut -d':' -f1)" 96 | cat "$webDocsFile" | tail -n +"$(($lastMetadataLine+2))" > "$webDocsFileTmp" 97 | mv "$webDocsFileTmp" "$webDocsFile" 98 | 99 | # Remove the top H1, as this will be added automatically on the web 100 | cat "$webDocsFile" | tail -n +3 > "$webDocsFileTmp" 101 | mv "$webDocsFileTmp" "$webDocsFile" 102 | 103 | # Rewrite Links 104 | rewriteLinks "$(cat "$webDocsFile")" "$4" > "$webDocsFileTmp" 105 | mv "$webDocsFileTmp" "$webDocsFile" 106 | } 107 | 108 | # Compiles the Packer SDC compiled docs folder down 109 | # to a integrations-compliant folder (web docs) 110 | # 111 | # $1: The directory of the plugin 112 | # $2: The directory of the SDC compiled docs files 113 | # $3: The output directory to place the web-docs files 114 | # $4: The org of the integration 115 | compileWebDocs() { 116 | local docsDir="$1/$2" 117 | local webDocsDir="$1/$3" 118 | 119 | echo "Compiling MDX docs in '$2' to Markdown in '$3'..." 120 | # Create the web-docs directory if it hasn't already been created 121 | mkdir -p "$webDocsDir" 122 | 123 | # Copy the README over 124 | cp "$docsDir/README.md" "$webDocsDir/README.md" 125 | 126 | # Process all MDX component files (exclude index files, which are unsupported) 127 | for file in $(find "$docsDir" | grep "$docsDir/.*/.*\.mdx" | grep --invert-match "index.mdx"); do 128 | processComponentFile "$docsDir" "$webDocsDir" "$file" "$4" 129 | done 130 | } 131 | 132 | compileWebDocs "$1" "$2" "$3" "$4" 133 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | See [Releases](https://github.com/hashicorp/packer-plugin-proxmox/releases) for latest CHANGELOG information. 2 | 3 | ## 1.0.1 (June 14, 2021) 4 | 5 | * Allow user to specify task_timeout [GH-#2] 6 | 7 | ## 1.0.0 (June 14, 2021) 8 | 9 | ### Enhancements: 10 | 11 | * Validate template_name and vm_name per proxmox requirements. [GH-15] 12 | * Update to latest Packer Plugin SDK [GH-19] 13 | 14 | ### Bug Fixes: 15 | 16 | * Fix qemu_agent to default to true when using HCL2 [GH-17] 17 | 18 | ## 0.0.2 (April 20, 2021) 19 | 20 | * Fast-follow release to resolve goreleaser issues. 21 | 22 | ## 0.0.1 (April 20, 2021) 23 | 24 | * Proxmox Plugin break out from Packer core. Changes prior to break out can be found in [Packer's CHANGELOG](https://github.com/hashicorp/packer/blob/master/CHANGELOG.md). 25 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @hashicorp/packer 2 | -------------------------------------------------------------------------------- /GNUmakefile: -------------------------------------------------------------------------------- 1 | NAME=proxmox 2 | BINARY=packer-plugin-${NAME} 3 | PLUGIN_FQN="$(shell grep -E '^module' **Note** 6 | > 7 | > Releases prior to v1.1.0 were incorrectly registered as `proxmox-promox-iso` and `proxmox-proxmox-clone`. 8 | > You are encouraged to upgrade any locally installed version of this plugin to v1.1.0. Refer to [Plugin loading issue](https://github.com/hashicorp/packer-plugin-proxmox/issues/119) for more details. 9 | 10 | 11 | ## Installation 12 | 13 | ### Using pre-built releases 14 | 15 | #### Using the `packer init` command 16 | 17 | Starting from version 1.7, Packer supports a new `packer init` command allowing 18 | automatic installation of Packer plugins. Read the 19 | [Packer documentation](https://www.packer.io/docs/commands/init) for more information. 20 | 21 | To install this plugin, copy and paste this code into your Packer configuration . 22 | Then, run [`packer init`](https://www.packer.io/docs/commands/init). 23 | 24 | ```hcl 25 | packer { 26 | required_plugins { 27 | proxmox = { 28 | version = ">= 1.2.2" 29 | source = "github.com/hashicorp/proxmox" 30 | } 31 | } 32 | } 33 | ``` 34 | 35 | 36 | #### Manual installation 37 | 38 | You can find pre-built binary releases of the plugin [here](https://github.com/hashicorp/packer-plugin-proxmox/releases). 39 | Once you have downloaded the latest archive corresponding to your target OS, 40 | uncompress it to retrieve the plugin binary file corresponding to your platform. 41 | To install the plugin, please follow the Packer documentation on 42 | [installing a plugin](https://www.packer.io/docs/extending/plugins/#installing-plugins). 43 | 44 | 45 | ### From Sources 46 | 47 | If you prefer to build the plugin from sources, clone the GitHub repository 48 | locally and run the command `go build` from the root 49 | directory. Upon successful compilation, a `packer-plugin-proxmox` plugin 50 | binary file can be found in the root directory. 51 | To install the compiled plugin, please follow the official Packer documentation 52 | on [installing a plugin](https://www.packer.io/docs/extending/plugins/#installing-plugins). 53 | 54 | 55 | ### Configuration 56 | 57 | For more information on how to configure the plugin, please read the 58 | documentation located in the [`docs/`](docs) directory. 59 | 60 | 61 | ## Contributing 62 | 63 | * If you think you've found a bug in the code or you have a question regarding 64 | the usage of this software, please reach out to us by opening an issue in 65 | this GitHub repository. 66 | * Contributions to this project are welcome: if you want to add a feature or a 67 | fix a bug, please do so by opening a Pull Request in this GitHub repository. 68 | In case of feature contribution, we kindly ask you to open an issue to 69 | discuss it beforehand. 70 | -------------------------------------------------------------------------------- /builder/proxmox/clone/builder.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package proxmoxclone 5 | 6 | import ( 7 | "crypto" 8 | "net/netip" 9 | "strings" 10 | 11 | proxmoxapi "github.com/Telmate/proxmox-api-go/proxmox" 12 | "github.com/hashicorp/hcl/v2/hcldec" 13 | proxmox "github.com/hashicorp/packer-plugin-proxmox/builder/proxmox/common" 14 | "github.com/hashicorp/packer-plugin-sdk/multistep" 15 | packersdk "github.com/hashicorp/packer-plugin-sdk/packer" 16 | 17 | "context" 18 | "fmt" 19 | ) 20 | 21 | // The unique id for the builder 22 | const BuilderID = "proxmox.clone" 23 | 24 | type Builder struct { 25 | config Config 26 | } 27 | 28 | // Builder implements packersdk.Builder 29 | var _ packersdk.Builder = &Builder{} 30 | 31 | func (b *Builder) ConfigSpec() hcldec.ObjectSpec { return b.config.FlatMapstructure().HCL2Spec() } 32 | 33 | func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) { 34 | return b.config.Prepare(raws...) 35 | } 36 | 37 | func (b *Builder) Run(ctx context.Context, ui packersdk.Ui, hook packersdk.Hook) (packersdk.Artifact, error) { 38 | state := new(multistep.BasicStateBag) 39 | state.Put("clone-config", &b.config) 40 | 41 | preSteps := []multistep.Step{ 42 | &StepSshKeyPair{ 43 | Debug: b.config.PackerDebug, 44 | DebugKeyPath: fmt.Sprintf("%s.pem", b.config.PackerBuildName), 45 | }, 46 | &StepMapSourceDisks{}, 47 | } 48 | postSteps := []multistep.Step{} 49 | 50 | sb := proxmox.NewSharedBuilder(BuilderID, b.config.Config, preSteps, postSteps, &cloneVMCreator{}) 51 | return sb.Run(ctx, ui, hook, state) 52 | } 53 | 54 | type cloneVMCreator struct{} 55 | 56 | func (*cloneVMCreator) Create(vmRef *proxmoxapi.VmRef, config proxmoxapi.ConfigQemu, state multistep.StateBag) error { 57 | client := state.Get("proxmoxClient").(*proxmoxapi.Client) 58 | c := state.Get("clone-config").(*Config) 59 | comm := state.Get("config").(*proxmox.Config).Comm 60 | 61 | fullClone := 1 62 | if c.FullClone.False() { 63 | fullClone = 0 64 | } 65 | config.FullClone = &fullClone 66 | 67 | // cloud-init options 68 | 69 | var nameServers []netip.Addr 70 | if c.Nameserver != "" { 71 | for _, nameserver := range strings.Split(c.Nameserver, " ") { 72 | ip, _ := netip.ParseAddr(nameserver) 73 | nameServers = append(nameServers, ip) 74 | } 75 | } 76 | 77 | IpconfigMap := proxmoxapi.CloudInitNetworkInterfaces{} 78 | for idx := range c.Ipconfigs { 79 | if c.Ipconfigs[idx] != (cloudInitIpconfig{}) { 80 | 81 | // backwards compatibility conversions 82 | 83 | var ipv4cfg proxmoxapi.CloudInitIPv4Config 84 | var ipv6cfg proxmoxapi.CloudInitIPv6Config 85 | 86 | // cloudInitIpconfig.Ip accepts a CIDR address or 'dhcp' string 87 | switch c.Ipconfigs[idx].Ip { 88 | case "dhcp": 89 | ipv4cfg.DHCP = true 90 | default: 91 | if c.Ipconfigs[idx].Ip != "" { 92 | addr := proxmoxapi.IPv4CIDR(c.Ipconfigs[idx].Ip) 93 | ipv4cfg.Address = &addr 94 | } 95 | } 96 | if c.Ipconfigs[idx].Gateway != "" { 97 | gw := proxmoxapi.IPv4Address(c.Ipconfigs[idx].Gateway) 98 | ipv4cfg.Gateway = &gw 99 | } 100 | 101 | // cloudInitIpconfig.Ip6 accepts a CIDR address, 'auto' or 'dhcp' string 102 | switch c.Ipconfigs[idx].Ip6 { 103 | case "dhcp": 104 | ipv6cfg.DHCP = true 105 | case "auto": 106 | ipv6cfg.SLAAC = true 107 | default: 108 | if c.Ipconfigs[idx].Ip6 != "" { 109 | addr := proxmoxapi.IPv6CIDR(c.Ipconfigs[idx].Ip6) 110 | ipv6cfg.Address = &addr 111 | } 112 | } 113 | if c.Ipconfigs[idx].Gateway6 != "" { 114 | addr := proxmoxapi.IPv6Address(c.Ipconfigs[idx].Gateway6) 115 | ipv6cfg.Gateway = &addr 116 | } 117 | 118 | IpconfigMap[proxmoxapi.QemuNetworkInterfaceID(idx)] = proxmoxapi.CloudInitNetworkConfig{ 119 | IPv4: &ipv4cfg, 120 | IPv6: &ipv6cfg, 121 | } 122 | } 123 | } 124 | 125 | var publicKey []crypto.PublicKey 126 | 127 | if comm.SSHPublicKey != nil { 128 | publicKey = append(publicKey, crypto.PublicKey(string(comm.SSHPublicKey))) 129 | } 130 | 131 | config.CloudInit = &proxmoxapi.CloudInit{ 132 | Username: &comm.SSHUsername, 133 | PublicSSHkeys: &publicKey, 134 | DNS: &proxmoxapi.GuestDNS{ 135 | NameServers: &nameServers, 136 | SearchDomain: &c.Searchdomain, 137 | }, 138 | NetworkInterfaces: IpconfigMap, 139 | } 140 | 141 | var sourceVmr *proxmoxapi.VmRef 142 | if c.CloneVM != "" { 143 | sourceVmrs, err := client.GetVmRefsByName(c.CloneVM) 144 | if err != nil { 145 | return err 146 | } 147 | 148 | // prefer source Vm located on same node 149 | sourceVmr = sourceVmrs[0] 150 | for _, candVmr := range sourceVmrs { 151 | if candVmr.Node() == vmRef.Node() { 152 | sourceVmr = candVmr 153 | } 154 | } 155 | } else if c.CloneVMID != 0 { 156 | sourceVmr = proxmoxapi.NewVmRef(c.CloneVMID) 157 | err := client.CheckVmRef(sourceVmr) 158 | if err != nil { 159 | return err 160 | } 161 | } 162 | 163 | err := config.CloneVm(sourceVmr, vmRef, client) 164 | if err != nil { 165 | return err 166 | } 167 | _, err = config.Update(false, vmRef, client) 168 | if err != nil { 169 | return err 170 | } 171 | return nil 172 | } 173 | -------------------------------------------------------------------------------- /builder/proxmox/clone/config.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | //go:generate packer-sdc struct-markdown 5 | //go:generate packer-sdc mapstructure-to-hcl2 -type Config,cloudInitIpconfig 6 | 7 | package proxmoxclone 8 | 9 | import ( 10 | "errors" 11 | "fmt" 12 | "net" 13 | "net/netip" 14 | "strings" 15 | 16 | proxmoxcommon "github.com/hashicorp/packer-plugin-proxmox/builder/proxmox/common" 17 | packersdk "github.com/hashicorp/packer-plugin-sdk/packer" 18 | "github.com/hashicorp/packer-plugin-sdk/template/config" 19 | ) 20 | 21 | type Config struct { 22 | proxmoxcommon.Config `mapstructure:",squash"` 23 | 24 | // The name of the VM Packer should clone and build from. 25 | // Either `clone_vm` or `clone_vm_id` must be specifed. 26 | CloneVM string `mapstructure:"clone_vm" required:"true"` 27 | // The ID of the VM Packer should clone and build from. 28 | // Proxmox VMIDs are limited to the range 100-999999999. 29 | // Either `clone_vm` or `clone_vm_id` must be specifed. 30 | CloneVMID int `mapstructure:"clone_vm_id" required:"true"` 31 | // Whether to run a full or shallow clone from the base clone_vm. Defaults to `true`. 32 | FullClone config.Trilean `mapstructure:"full_clone" required:"false"` 33 | 34 | // Set nameserver IP address(es) via Cloud-Init. 35 | // If not given, the same setting as on the host is used. 36 | Nameserver string `mapstructure:"nameserver" required:"false"` 37 | // Set the DNS searchdomain via Cloud-Init. 38 | // If not given, the same setting as on the host is used. 39 | Searchdomain string `mapstructure:"searchdomain" required:"false"` 40 | // Set IP address and gateway via Cloud-Init. 41 | // See the [CloudInit Ip Configuration](#cloudinit-ip-configuration) documentation for fields. 42 | Ipconfigs []cloudInitIpconfig `mapstructure:"ipconfig" required:"false"` 43 | } 44 | 45 | // If you have configured more than one network interface, make sure to match the order of 46 | // `network_adapters` and `ipconfig`. 47 | // 48 | // Usage example (JSON): 49 | // 50 | // ```json 51 | // [ 52 | // 53 | // { 54 | // "ip": "192.168.1.55/24", 55 | // "gateway": "192.168.1.1", 56 | // "ip6": "fda8:a260:6eda:20::4da/128", 57 | // "gateway6": "fda8:a260:6eda:20::1" 58 | // } 59 | // 60 | // ] 61 | // ``` 62 | type cloudInitIpconfig struct { 63 | // Either an IPv4 address (CIDR notation) or `dhcp`. 64 | Ip string `mapstructure:"ip" required:"false"` 65 | // IPv4 gateway. 66 | Gateway string `mapstructure:"gateway" required:"false"` 67 | // Can be an IPv6 address (CIDR notation), `auto` (enables SLAAC), or `dhcp`. 68 | Ip6 string `mapstructure:"ip6" required:"false"` 69 | // IPv6 gateway. 70 | Gateway6 string `mapstructure:"gateway6" required:"false"` 71 | } 72 | 73 | func (c *Config) Prepare(raws ...interface{}) ([]string, []string, error) { 74 | var errs *packersdk.MultiError 75 | _, warnings, merrs := c.Config.Prepare(c, raws...) 76 | if merrs != nil { 77 | errs = packersdk.MultiErrorAppend(errs, merrs) 78 | } 79 | 80 | if c.CloneVM == "" && c.CloneVMID == 0 { 81 | errs = packersdk.MultiErrorAppend(errs, errors.New("one of clone_vm or clone_vm_id must be specified")) 82 | } 83 | if c.CloneVM != "" && c.CloneVMID != 0 { 84 | errs = packersdk.MultiErrorAppend(errs, errors.New("clone_vm and clone_vm_id cannot both be specified")) 85 | } 86 | // Technically Proxmox VMIDs are unsigned 32bit integers, but are limited to 87 | // the range 100-999999999. Source: 88 | // https://pve-devel.pve.proxmox.narkive.com/Pa6mH1OP/avoiding-vmid-reuse#post8 89 | if c.CloneVMID != 0 && (c.CloneVMID < 100 || c.CloneVMID > 999999999) { 90 | errs = packersdk.MultiErrorAppend(errs, errors.New("clone_vm_id must be in range 100-999999999")) 91 | } 92 | 93 | // Check validity of given IP addresses 94 | if c.Nameserver != "" { 95 | for _, nameserver := range strings.Split(c.Nameserver, " ") { 96 | _, err := netip.ParseAddr(nameserver) 97 | if err != nil { 98 | errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("could not parse nameserver: %s", err)) 99 | } 100 | } 101 | } 102 | for _, i := range c.Ipconfigs { 103 | if i.Ip != "" && i.Ip != "dhcp" { 104 | _, _, err := net.ParseCIDR(i.Ip) 105 | if err != nil { 106 | errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("could not parse ipconfig.ip: %s", err)) 107 | } 108 | } 109 | if i.Gateway != "" { 110 | _, err := netip.ParseAddr(i.Gateway) 111 | if err != nil { 112 | errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("could not parse ipconfig.gateway: %s", err)) 113 | } 114 | } 115 | if i.Ip6 != "" && i.Ip6 != "auto" && i.Ip6 != "dhcp" { 116 | _, _, err := net.ParseCIDR(i.Ip6) 117 | if err != nil { 118 | errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("could not parse ipconfig.ip6: %s", err)) 119 | } 120 | } 121 | if i.Gateway6 != "" { 122 | _, err := netip.ParseAddr(i.Gateway6) 123 | if err != nil { 124 | errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("could not parse ipconfig.gateway6: %s", err)) 125 | } 126 | } 127 | } 128 | if len(c.NICs) < len(c.Ipconfigs) { 129 | errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("%d ipconfig blocks given, but only %d network interfaces defined", len(c.Ipconfigs), len(c.NICs))) 130 | } 131 | 132 | if errs != nil && len(errs.Errors) > 0 { 133 | return nil, warnings, errs 134 | } 135 | return nil, warnings, nil 136 | } 137 | 138 | // Convert Ipconfig attributes into a Proxmox-API compatible string 139 | func (c cloudInitIpconfig) String() string { 140 | options := []string{} 141 | if c.Ip != "" { 142 | options = append(options, "ip="+c.Ip) 143 | } 144 | if c.Gateway != "" { 145 | options = append(options, "gw="+c.Gateway) 146 | } 147 | if c.Ip6 != "" { 148 | options = append(options, "ip6="+c.Ip6) 149 | } 150 | if c.Gateway6 != "" { 151 | options = append(options, "gw6="+c.Gateway6) 152 | } 153 | return strings.Join(options, ",") 154 | } 155 | -------------------------------------------------------------------------------- /builder/proxmox/clone/config_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package proxmoxclone 5 | 6 | import ( 7 | "strings" 8 | "testing" 9 | 10 | proxmox "github.com/hashicorp/packer-plugin-proxmox/builder/proxmox/common" 11 | packersdk "github.com/hashicorp/packer-plugin-sdk/packer" 12 | ) 13 | 14 | func mandatoryConfig(t *testing.T) map[string]interface{} { 15 | return map[string]interface{}{ 16 | "proxmox_url": "https://my-proxmox.my-domain:8006/api2/json", 17 | "username": "apiuser@pve", 18 | "token": "xxxx-xxxx-xxxx-xxxx", 19 | "node": "my-proxmox", 20 | "ssh_username": "root", 21 | "clone_vm": "MyTemplate", 22 | } 23 | } 24 | 25 | func TestRequiredParameters(t *testing.T) { 26 | var c Config 27 | _, _, err := c.Prepare(&c, make(map[string]interface{})) 28 | if err == nil { 29 | t.Fatal("Expected empty configuration to fail") 30 | } 31 | errs, ok := err.(*packersdk.MultiError) 32 | if !ok { 33 | t.Fatal("Expected errors to be packersdk.MultiError") 34 | } 35 | 36 | required := []string{"username", "token", "proxmox_url", "node", "ssh_username", "clone_vm", "clone_vm_id"} 37 | for _, param := range required { 38 | found := false 39 | for _, err := range errs.Errors { 40 | if strings.Contains(err.Error(), param) { 41 | found = true 42 | break 43 | } 44 | } 45 | if !found { 46 | t.Errorf("Expected error about missing parameters %q", param) 47 | } 48 | } 49 | } 50 | 51 | func TestVMNameOrID(t *testing.T) { 52 | ipconfigTest := []struct { 53 | name string 54 | cloneVM string 55 | cloneVMID int 56 | expectFailure bool 57 | }{ 58 | { 59 | name: "clone_vm given, no error", 60 | cloneVM: "myVM", 61 | cloneVMID: 0, 62 | expectFailure: false, 63 | }, 64 | { 65 | name: "valid clone_vm_id given, no error", 66 | cloneVM: "", 67 | cloneVMID: 100, 68 | expectFailure: false, 69 | }, 70 | { 71 | name: "clone_vm_id out of range, error", 72 | cloneVM: "", 73 | cloneVMID: 50, 74 | expectFailure: true, 75 | }, 76 | { 77 | name: "clone_vm and clone_vm_id given, error", 78 | cloneVM: "myVM", 79 | cloneVMID: 100, 80 | expectFailure: true, 81 | }, 82 | { 83 | name: "neither clone_vm nor clone_vm_id given, error", 84 | cloneVM: "", 85 | cloneVMID: 0, 86 | expectFailure: true, 87 | }, 88 | } 89 | 90 | for _, tt := range ipconfigTest { 91 | t.Run(tt.name, func(t *testing.T) { 92 | cfg := mandatoryConfig(t) 93 | cfg["clone_vm"] = tt.cloneVM 94 | cfg["clone_vm_id"] = tt.cloneVMID 95 | 96 | var c Config 97 | _, _, err := c.Prepare(&c, cfg) 98 | if err != nil && !tt.expectFailure { 99 | t.Fatalf("unexpected failure: %s", err) 100 | } 101 | if err == nil && tt.expectFailure { 102 | t.Errorf("expected failure, but prepare succeeded") 103 | } 104 | }) 105 | } 106 | } 107 | 108 | func TestNameserver(t *testing.T) { 109 | ipconfigTest := []struct { 110 | name string 111 | nameserver string 112 | expectFailure bool 113 | }{ 114 | { 115 | name: "nameserver empty, no error", 116 | expectFailure: false, 117 | nameserver: "", 118 | }, 119 | { 120 | name: "single valid nameserver, no error", 121 | expectFailure: false, 122 | nameserver: "192.168.1.1", 123 | }, 124 | { 125 | name: "two valid nameservers, no error", 126 | expectFailure: false, 127 | nameserver: "192.168.1.1 192.168.1.2", 128 | }, 129 | { 130 | name: "comma separated nameservers, fail", 131 | expectFailure: true, 132 | nameserver: "192.168.1.1,192.168.1.2", 133 | }, 134 | { 135 | name: "invalid nameserver, fail", 136 | expectFailure: true, 137 | nameserver: "192.168.1", 138 | }, 139 | } 140 | 141 | for _, tt := range ipconfigTest { 142 | t.Run(tt.name, func(t *testing.T) { 143 | cfg := mandatoryConfig(t) 144 | cfg["nameserver"] = tt.nameserver 145 | 146 | var c Config 147 | _, _, err := c.Prepare(&c, cfg) 148 | if err != nil && !tt.expectFailure { 149 | t.Fatalf("unexpected failure: %s", err) 150 | } 151 | if err == nil && tt.expectFailure { 152 | t.Errorf("expected failure, but prepare succeeded") 153 | } 154 | }) 155 | } 156 | } 157 | 158 | func TestIpconfig(t *testing.T) { 159 | ipconfigTest := []struct { 160 | name string 161 | nics []proxmox.NICConfig 162 | ipconfigs []cloudInitIpconfig 163 | expectFailure bool 164 | }{ 165 | { 166 | name: "ipconfig empty, no error", 167 | expectFailure: false, 168 | ipconfigs: []cloudInitIpconfig{}, 169 | }, 170 | { 171 | name: "valid ipconfig, no error", 172 | expectFailure: false, 173 | ipconfigs: []cloudInitIpconfig{ 174 | { 175 | Ip: "192.168.1.55/24", 176 | Gateway: "192.168.1.1", 177 | Ip6: "fda8:a260:6eda:20::4da/128", 178 | Gateway6: "fda8:a260:6eda:20::1", 179 | }, 180 | }, 181 | nics: []proxmox.NICConfig{ 182 | { 183 | Model: "virtio", 184 | Bridge: "vmbr0", 185 | }, 186 | }, 187 | }, 188 | { 189 | name: "IPv4 invalid CIDR, fail", 190 | expectFailure: true, 191 | ipconfigs: []cloudInitIpconfig{ 192 | { 193 | Ip: "192.168.1.55", 194 | Gateway: "192.168.1.1", 195 | }, 196 | }, 197 | nics: []proxmox.NICConfig{ 198 | { 199 | Model: "virtio", 200 | Bridge: "vmbr0", 201 | }, 202 | }, 203 | }, 204 | { 205 | name: "IPv6 invalid CIDR, fail", 206 | expectFailure: true, 207 | ipconfigs: []cloudInitIpconfig{ 208 | { 209 | Ip6: "fda8:a260:6eda:20::4da", 210 | Gateway6: "fda8:a260:6eda:20::1", 211 | }, 212 | }, 213 | nics: []proxmox.NICConfig{ 214 | { 215 | Model: "virtio", 216 | Bridge: "vmbr0", 217 | }, 218 | }, 219 | }, 220 | { 221 | name: "not enough nics, fail", 222 | expectFailure: true, 223 | ipconfigs: []cloudInitIpconfig{ 224 | { 225 | Ip6: "fda8:a260:6eda:20::4da/128", 226 | Gateway6: "fda8:a260:6eda:20::1", 227 | }, 228 | { 229 | Ip6: "fda8:a260:6eda:20::4db/128", 230 | Gateway6: "fda8:a260:6eda:20::1", 231 | }, 232 | }, 233 | nics: []proxmox.NICConfig{ 234 | { 235 | Model: "virtio", 236 | Bridge: "vmbr0", 237 | }, 238 | }, 239 | }, 240 | { 241 | name: "ipconfig DHCP, no error", 242 | expectFailure: false, 243 | ipconfigs: []cloudInitIpconfig{ 244 | { 245 | Ip: "dhcp", 246 | Ip6: "dhcp", 247 | }, 248 | }, 249 | nics: []proxmox.NICConfig{ 250 | { 251 | Model: "virtio", 252 | Bridge: "vmbr0", 253 | }, 254 | }, 255 | }, 256 | } 257 | 258 | for _, tt := range ipconfigTest { 259 | t.Run(tt.name, func(t *testing.T) { 260 | cfg := mandatoryConfig(t) 261 | cfg["network_adapters"] = tt.nics 262 | cfg["ipconfig"] = tt.ipconfigs 263 | 264 | var c Config 265 | _, _, err := c.Prepare(&c, cfg) 266 | if err != nil && !tt.expectFailure { 267 | t.Fatalf("unexpected failure: %s", err) 268 | } 269 | if err == nil && tt.expectFailure { 270 | t.Errorf("expected failure, but prepare succeeded") 271 | } 272 | }) 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /builder/proxmox/clone/step_map_source_disks.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package proxmoxclone 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "log" 10 | "regexp" 11 | "strings" 12 | 13 | proxmoxapi "github.com/Telmate/proxmox-api-go/proxmox" 14 | proxmox "github.com/hashicorp/packer-plugin-proxmox/builder/proxmox/common" 15 | "github.com/hashicorp/packer-plugin-sdk/multistep" 16 | packersdk "github.com/hashicorp/packer-plugin-sdk/packer" 17 | ) 18 | 19 | // StepMapSourceDisks retrieves the configuration of the clone source vm 20 | // and identifies any attached disks to prevent hcl/json defined disks 21 | // and isos from overwriting their assignments. 22 | // (Enables append behavior for hcl/json defined disks and ISOs) 23 | type StepMapSourceDisks struct{} 24 | 25 | type cloneSource interface { 26 | GetVmConfig(*proxmoxapi.VmRef) (map[string]interface{}, error) 27 | GetVmRefsByName(string) ([]*proxmoxapi.VmRef, error) 28 | CheckVmRef(*proxmoxapi.VmRef) error 29 | } 30 | 31 | var _ cloneSource = &proxmoxapi.Client{} 32 | 33 | func (s *StepMapSourceDisks) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { 34 | ui := state.Get("ui").(packersdk.Ui) 35 | client := state.Get("proxmoxClient").(cloneSource) 36 | c := state.Get("clone-config").(*Config) 37 | 38 | var sourceVmr *proxmoxapi.VmRef 39 | if c.CloneVM != "" { 40 | sourceVmrs, err := client.GetVmRefsByName(c.CloneVM) 41 | if err != nil { 42 | state.Put("error", fmt.Errorf("Could not retrieve VM: %s", err)) 43 | return multistep.ActionHalt 44 | } 45 | // prefer source Vm located on same node 46 | sourceVmr = sourceVmrs[0] 47 | for _, candVmr := range sourceVmrs { 48 | if candVmr.Node() == c.Node { 49 | sourceVmr = candVmr 50 | } 51 | } 52 | } else if c.CloneVMID != 0 { 53 | sourceVmr = proxmoxapi.NewVmRef(c.CloneVMID) 54 | err := client.CheckVmRef(sourceVmr) 55 | if err != nil { 56 | state.Put("error", fmt.Errorf("Could not retrieve VM: %s", err)) 57 | return multistep.ActionHalt 58 | } 59 | } 60 | 61 | vmParams, err := client.GetVmConfig(sourceVmr) 62 | if err != nil { 63 | err := fmt.Errorf("error fetching template config: %s", err) 64 | state.Put("error", err) 65 | ui.Error(err.Error()) 66 | return multistep.ActionHalt 67 | } 68 | 69 | var sourceDisks []string 70 | 71 | // example v data returned for a disk: 72 | // local-lvm:base-9100-disk-1,backup=0,cache=none,discard=on,replicate=0,size=16G 73 | // example v data returned for a cloud-init disk: 74 | // local-lvm:vm-9100-cloudinit,media=cdrom 75 | // example v data returned for a cdrom: 76 | // local-lvm:iso/ubuntu-14.04.1-server-amd64.iso,media=cdrom,size=572M 77 | 78 | // preserve only disk assignments, cloud-init drives are recreated by common builder 79 | for k, v := range vmParams { 80 | // get device from k eg. ide from ide2 81 | rd := regexp.MustCompile(`\D+`) 82 | switch rd.FindString(k) { 83 | case "ide", "sata", "scsi", "virtio": 84 | if !strings.Contains(v.(string), "media=cdrom") { 85 | log.Println("disk discovered on source vm at", k) 86 | sourceDisks = append(sourceDisks, k) 87 | } 88 | } 89 | } 90 | 91 | // store discovered disks in common config 92 | d := state.Get("config").(*proxmox.Config) 93 | d.CloneSourceDisks = sourceDisks 94 | 95 | return multistep.ActionContinue 96 | } 97 | 98 | func (s *StepMapSourceDisks) Cleanup(state multistep.StateBag) {} 99 | -------------------------------------------------------------------------------- /builder/proxmox/clone/step_ssh_key_pair.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package proxmoxclone 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "os" 10 | 11 | common "github.com/hashicorp/packer-plugin-proxmox/builder/proxmox/common" 12 | "github.com/hashicorp/packer-plugin-sdk/communicator/ssh" 13 | "github.com/hashicorp/packer-plugin-sdk/multistep" 14 | packersdk "github.com/hashicorp/packer-plugin-sdk/packer" 15 | "github.com/hashicorp/packer-plugin-sdk/uuid" 16 | ) 17 | 18 | // StepSshKeyPair executes the business logic for setting the SSH key pair in 19 | // the specified communicator.Config. 20 | type StepSshKeyPair struct { 21 | Debug bool 22 | DebugKeyPath string 23 | } 24 | 25 | func (s *StepSshKeyPair) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { 26 | ui := state.Get("ui").(packersdk.Ui) 27 | c := state.Get("config").(*common.Config) 28 | 29 | if c.Comm.SSHPassword != "" { 30 | return multistep.ActionContinue 31 | } 32 | 33 | if c.Comm.SSHPrivateKeyFile != "" { 34 | ui.Say("Using existing SSH private key for the communicator...") 35 | privateKeyBytes, err := c.Comm.ReadSSHPrivateKeyFile() 36 | if err != nil { 37 | state.Put("error", err) 38 | return multistep.ActionHalt 39 | } 40 | 41 | kp, err := ssh.KeyPairFromPrivateKey(ssh.FromPrivateKeyConfig{ 42 | RawPrivateKeyPemBlock: privateKeyBytes, 43 | Comment: fmt.Sprintf("packer_%s", uuid.TimeOrderedUUID()), 44 | }) 45 | if err != nil { 46 | state.Put("error", err) 47 | return multistep.ActionHalt 48 | } 49 | 50 | c.Comm.SSHPrivateKey = privateKeyBytes 51 | c.Comm.SSHKeyPairName = kp.Comment 52 | c.Comm.SSHTemporaryKeyPairName = kp.Comment 53 | c.Comm.SSHPublicKey = kp.PublicKeyAuthorizedKeysLine 54 | 55 | return multistep.ActionContinue 56 | } 57 | 58 | if c.Comm.SSHAgentAuth { 59 | ui.Say("Using local SSH Agent to authenticate connections for the communicator...") 60 | return multistep.ActionContinue 61 | } 62 | 63 | ui.Say("Creating ephemeral key pair for SSH communicator...") 64 | 65 | kp, err := ssh.NewKeyPair(ssh.CreateKeyPairConfig{ 66 | Comment: fmt.Sprintf("packer_%s", uuid.TimeOrderedUUID()), 67 | }) 68 | if err != nil { 69 | state.Put("error", fmt.Errorf("Error creating temporary keypair: %s", err)) 70 | return multistep.ActionHalt 71 | } 72 | 73 | c.Comm.SSHKeyPairName = kp.Comment 74 | c.Comm.SSHTemporaryKeyPairName = kp.Comment 75 | c.Comm.SSHPrivateKey = kp.PrivateKeyPemBlock 76 | c.Comm.SSHPublicKey = kp.PublicKeyAuthorizedKeysLine 77 | c.Comm.SSHClearAuthorizedKeys = true 78 | 79 | ui.Say("Created ephemeral SSH key pair for communicator") 80 | 81 | // If we're in debug mode, output the private key to the working 82 | // directory. 83 | if s.Debug { 84 | ui.Message(fmt.Sprintf("Saving communicator private key for debug purposes: %s", s.DebugKeyPath)) 85 | f, err := os.OpenFile(s.DebugKeyPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) 86 | if err != nil { 87 | state.Put("error", fmt.Errorf("Error saving debug key: %s", err)) 88 | return multistep.ActionHalt 89 | } 90 | defer f.Close() 91 | 92 | // Write the key out 93 | if _, err := f.Write(kp.PrivateKeyPemBlock); err != nil { 94 | state.Put("error", fmt.Errorf("Error saving debug key: %s", err)) 95 | return multistep.ActionHalt 96 | } 97 | } 98 | 99 | return multistep.ActionContinue 100 | } 101 | 102 | func (s *StepSshKeyPair) Cleanup(state multistep.StateBag) { 103 | if s.Debug { 104 | if err := os.Remove(s.DebugKeyPath); err != nil { 105 | ui := state.Get("ui").(packersdk.Ui) 106 | ui.Error(fmt.Sprintf( 107 | "Error removing debug key '%s': %s", s.DebugKeyPath, err)) 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /builder/proxmox/common/artifact.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package proxmox 5 | 6 | import ( 7 | "fmt" 8 | "log" 9 | "strconv" 10 | 11 | "github.com/Telmate/proxmox-api-go/proxmox" 12 | packersdk "github.com/hashicorp/packer-plugin-sdk/packer" 13 | ) 14 | 15 | type Artifact struct { 16 | builderID string 17 | templateID int 18 | proxmoxClient *proxmox.Client 19 | 20 | // StateData should store data such as GeneratedData 21 | // to be shared with post-processors 22 | StateData map[string]interface{} 23 | } 24 | 25 | // Artifact implements packersdk.Artifact 26 | var _ packersdk.Artifact = &Artifact{} 27 | 28 | func (a *Artifact) BuilderId() string { 29 | return a.builderID 30 | } 31 | 32 | func (*Artifact) Files() []string { 33 | return nil 34 | } 35 | 36 | func (a *Artifact) Id() string { 37 | return strconv.Itoa(a.templateID) 38 | } 39 | 40 | func (a *Artifact) String() string { 41 | return fmt.Sprintf("A template was created: %d", a.templateID) 42 | } 43 | 44 | func (a *Artifact) State(name string) interface{} { 45 | return a.StateData[name] 46 | } 47 | 48 | func (a *Artifact) Destroy() error { 49 | log.Printf("Destroying template: %d", a.templateID) 50 | _, err := a.proxmoxClient.DeleteVm(proxmox.NewVmRef(a.templateID)) 51 | return err 52 | } 53 | -------------------------------------------------------------------------------- /builder/proxmox/common/bootcommand_driver.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package proxmox 5 | 6 | import ( 7 | "fmt" 8 | "strings" 9 | "time" 10 | "unicode" 11 | 12 | "github.com/Telmate/proxmox-api-go/proxmox" 13 | "github.com/hashicorp/packer-plugin-sdk/bootcommand" 14 | ) 15 | 16 | type proxmoxDriver struct { 17 | client commandTyper 18 | vmRef *proxmox.VmRef 19 | specialMap map[string]string 20 | runeMap map[rune]string 21 | interval time.Duration 22 | specialBuffer []string 23 | normalBuffer []string 24 | } 25 | 26 | func NewProxmoxDriver(c commandTyper, vmRef *proxmox.VmRef, interval time.Duration) *proxmoxDriver { 27 | // Mappings for packer shorthand to qemu qkeycodes 28 | sMap := map[string]string{ 29 | "spacebar": "spc", 30 | "bs": "backspace", 31 | "del": "delete", 32 | "return": "ret", 33 | "enter": "ret", 34 | "pageUp": "pgup", 35 | "pageDown": "pgdn", 36 | "leftshift": "shift", 37 | "rightshift": "shift", 38 | "leftalt": "alt", 39 | "rightalt": "alt_r", 40 | "leftctrl": "ctrl", 41 | "rightctrl": "ctrl_r", 42 | "leftsuper": "meta_l", 43 | "rightsuper": "meta_r", 44 | } 45 | // Mappings for runes that need to be translated to special qkeycodes 46 | // Taken from https://github.com/qemu/qemu/blob/master/pc-bios/keymaps/en-us 47 | rMap := map[rune]string{ 48 | // Clean mappings 49 | ' ': "spc", 50 | '.': "dot", 51 | ',': "comma", 52 | ';': "semicolon", 53 | '*': "asterisk", 54 | '-': "minus", 55 | '[': "bracket_left", 56 | ']': "bracket_right", 57 | '=': "equal", 58 | '\'': "apostrophe", 59 | '`': "grave_accent", 60 | '/': "slash", 61 | '\\': "backslash", 62 | 63 | '!': "shift-1", // "exclam" 64 | '@': "shift-2", // "at" 65 | '#': "shift-3", // "numbersign" 66 | '$': "shift-4", // "dollar" 67 | '%': "shift-5", // "percent" 68 | '^': "shift-6", // "asciicircum" 69 | '&': "shift-7", // "ampersand" 70 | '(': "shift-9", // "parenleft" 71 | ')': "shift-0", // "parenright" 72 | '{': "shift-bracket_left", // "braceleft" 73 | '}': "shift-bracket_right", // "braceright" 74 | '"': "shift-apostrophe", // "quotedbl" 75 | '+': "shift-equal", // "plus" 76 | '_': "shift-minus", // "underscore" 77 | ':': "shift-semicolon", // "colon" 78 | '<': "shift-comma", // "less" is recognized, but seem to map to '/'? 79 | '>': "shift-dot", // "greater" 80 | '~': "shift-grave_accent", // "asciitilde" 81 | '?': "shift-slash", // "question" 82 | '|': "shift-backslash", // "bar" 83 | } 84 | 85 | return &proxmoxDriver{ 86 | client: c, 87 | vmRef: vmRef, 88 | specialMap: sMap, 89 | runeMap: rMap, 90 | interval: interval, 91 | } 92 | } 93 | 94 | func (p *proxmoxDriver) SendKey(key rune, action bootcommand.KeyAction) error { 95 | switch action.String() { 96 | case "Press": 97 | if special, ok := p.runeMap[key]; ok { 98 | return p.send(special) 99 | } 100 | var keys string 101 | if unicode.IsUpper(key) { 102 | keys = fmt.Sprintf("shift-%c", unicode.ToLower(key)) 103 | } else { 104 | keys = fmt.Sprintf("%c", key) 105 | } 106 | return p.send(keys) 107 | case "On": 108 | key := fmt.Sprintf("%c", key) 109 | p.normalBuffer = addKeyToBuffer(p.normalBuffer, key) 110 | case "Off": 111 | key := fmt.Sprintf("%c", key) 112 | p.normalBuffer = removeKeyFromBuffer(p.normalBuffer, key) 113 | } 114 | return nil 115 | } 116 | 117 | func (p *proxmoxDriver) SendSpecial(special string, action bootcommand.KeyAction) error { 118 | keys := special 119 | if replacement, ok := p.specialMap[special]; ok { 120 | keys = replacement 121 | } 122 | switch action.String() { 123 | case "Press": 124 | return p.send(keys) 125 | case "On": 126 | p.specialBuffer = addKeyToBuffer(p.specialBuffer, keys) 127 | case "Off": 128 | p.specialBuffer = removeKeyFromBuffer(p.specialBuffer, keys) 129 | } 130 | return nil 131 | } 132 | 133 | func (p *proxmoxDriver) send(key string) error { 134 | keys := append(p.specialBuffer, p.normalBuffer...) 135 | keys = append(keys, key) 136 | keyEventString := bufferToKeyEvent(keys) 137 | err := p.client.Sendkey(p.vmRef, keyEventString) 138 | if err != nil { 139 | return err 140 | } 141 | time.Sleep(p.interval) 142 | return nil 143 | } 144 | 145 | func (p *proxmoxDriver) Flush() error { return nil } 146 | 147 | func bufferToKeyEvent(keys []string) string { 148 | return strings.Join(keys, "-") 149 | } 150 | func addKeyToBuffer(buffer []string, key string) []string { 151 | for _, value := range buffer { 152 | if value == key { 153 | return buffer 154 | } 155 | } 156 | return append(buffer, key) 157 | } 158 | func removeKeyFromBuffer(buffer []string, key string) []string { 159 | for index, value := range buffer { 160 | if value == key { 161 | buffer[index] = buffer[len(buffer)-1] 162 | return buffer[:len(buffer)-1] 163 | } 164 | } 165 | return buffer 166 | } 167 | -------------------------------------------------------------------------------- /builder/proxmox/common/builder.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package proxmox 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | "fmt" 10 | 11 | "github.com/Telmate/proxmox-api-go/proxmox" 12 | "github.com/hashicorp/packer-plugin-sdk/communicator" 13 | "github.com/hashicorp/packer-plugin-sdk/multistep" 14 | "github.com/hashicorp/packer-plugin-sdk/multistep/commonsteps" 15 | packersdk "github.com/hashicorp/packer-plugin-sdk/packer" 16 | ) 17 | 18 | func NewSharedBuilder(id string, config Config, preSteps []multistep.Step, postSteps []multistep.Step, vmCreator ProxmoxVMCreator) *Builder { 19 | return &Builder{ 20 | id: id, 21 | config: config, 22 | preSteps: preSteps, 23 | postSteps: postSteps, 24 | vmCreator: vmCreator, 25 | } 26 | } 27 | 28 | type Builder struct { 29 | id string 30 | config Config 31 | preSteps []multistep.Step 32 | postSteps []multistep.Step 33 | runner multistep.Runner 34 | proxmoxClient *proxmox.Client 35 | vmCreator ProxmoxVMCreator 36 | } 37 | 38 | func (b *Builder) Run(ctx context.Context, ui packersdk.Ui, hook packersdk.Hook, state multistep.StateBag) (packersdk.Artifact, error) { 39 | var err error 40 | b.proxmoxClient, err = newProxmoxClient(b.config) 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | // Set up the state 46 | state.Put("config", &b.config) 47 | state.Put("proxmoxClient", b.proxmoxClient) 48 | state.Put("hook", hook) 49 | state.Put("ui", ui) 50 | 51 | comm := &b.config.Comm 52 | 53 | // Build the steps 54 | coreSteps := []multistep.Step{ 55 | &stepStartVM{ 56 | vmCreator: b.vmCreator, 57 | }, 58 | commonsteps.HTTPServerFromHTTPConfig(&b.config.HTTPConfig), 59 | &stepTypeBootCommand{ 60 | BootConfig: b.config.BootConfig, 61 | Ctx: b.config.Ctx, 62 | }, 63 | &communicator.StepConnect{ 64 | Config: comm, 65 | Host: commHost((*comm).Host()), 66 | SSHConfig: (*comm).SSHConfigFunc(), 67 | }, 68 | &commonsteps.StepProvision{}, 69 | &commonsteps.StepCleanupTempKeys{ 70 | Comm: &b.config.Comm, 71 | }, 72 | &stepRemoveCloudInitDrive{}, 73 | &stepConvertToTemplate{}, 74 | &stepFinalizeTemplateConfig{}, 75 | &stepSuccess{}, 76 | } 77 | preSteps := b.preSteps 78 | for idx := range b.config.ISOs { 79 | if b.config.ISOs[idx].ISODownloadPVE { 80 | preSteps = append(preSteps, 81 | &stepDownloadISOOnPVE{ 82 | ISO: &b.config.ISOs[idx], 83 | }, 84 | ) 85 | } else { 86 | preSteps = append(preSteps, 87 | &commonsteps.StepCreateCD{ 88 | Files: b.config.ISOs[idx].CDConfig.CDFiles, 89 | Content: b.config.ISOs[idx].CDConfig.CDContent, 90 | Label: b.config.ISOs[idx].CDConfig.CDLabel, 91 | }, 92 | &commonsteps.StepDownload{ 93 | Checksum: b.config.ISOs[idx].ISOChecksum, 94 | Description: "ISO", 95 | Extension: b.config.ISOs[idx].TargetExtension, 96 | ResultKey: b.config.ISOs[idx].DownloadPathKey, 97 | TargetPath: b.config.ISOs[idx].DownloadPathKey, 98 | Url: b.config.ISOs[idx].ISOUrls, 99 | }, 100 | &stepUploadISO{ 101 | ISO: &b.config.ISOs[idx], 102 | }, 103 | ) 104 | } 105 | } 106 | 107 | steps := append(preSteps, coreSteps...) 108 | steps = append(steps, b.postSteps...) 109 | // Run the steps 110 | b.runner = commonsteps.NewRunner(steps, b.config.PackerConfig, ui) 111 | b.runner.Run(ctx, state) 112 | // If there was an error, return that 113 | if rawErr, ok := state.GetOk("error"); ok { 114 | return nil, rawErr.(error) 115 | } 116 | // If we were interrupted or cancelled, then just exit. 117 | if _, ok := state.GetOk(multistep.StateCancelled); ok { 118 | return nil, errors.New("build was cancelled") 119 | } 120 | 121 | // Verify that the template_id was set properly, otherwise we didn't progress through the last step 122 | tplID, ok := state.Get("template_id").(int) 123 | if !ok { 124 | return nil, fmt.Errorf("template ID could not be determined") 125 | } 126 | 127 | artifact := &Artifact{ 128 | builderID: b.id, 129 | templateID: tplID, 130 | proxmoxClient: b.proxmoxClient, 131 | StateData: map[string]interface{}{"generated_data": state.Get("generated_data")}, 132 | } 133 | 134 | return artifact, nil 135 | } 136 | 137 | // Returns ssh_host or winrm_host (see communicator.Config.Host) config 138 | // parameter when set, otherwise gets the host IP from running VM 139 | func commHost(host string) func(state multistep.StateBag) (string, error) { 140 | if host != "" { 141 | return func(state multistep.StateBag) (string, error) { 142 | return host, nil 143 | } 144 | } 145 | return getVMIP 146 | } 147 | 148 | // Reads the first non-loopback interface's IP address from the VM. 149 | // qemu-guest-agent package must be installed on the VM 150 | func getVMIP(state multistep.StateBag) (string, error) { 151 | client := state.Get("proxmoxClient").(*proxmox.Client) 152 | config := state.Get("config").(*Config) 153 | vmRef := state.Get("vmRef").(*proxmox.VmRef) 154 | 155 | ifs, err := client.GetVmAgentNetworkInterfaces(vmRef) 156 | if err != nil { 157 | return "", err 158 | } 159 | 160 | if config.VMInterface != "" { 161 | for _, iface := range ifs { 162 | if config.VMInterface != iface.Name { 163 | continue 164 | } 165 | 166 | for _, addr := range iface.IpAddresses { 167 | if addr.IsLoopback() { 168 | continue 169 | } 170 | return addr.String(), nil 171 | } 172 | return "", fmt.Errorf("Interface %s only has loopback addresses", config.VMInterface) 173 | } 174 | return "", fmt.Errorf("Interface %s not found in VM", config.VMInterface) 175 | } 176 | 177 | for _, iface := range ifs { 178 | for _, addr := range iface.IpAddresses { 179 | if addr.IsLoopback() { 180 | continue 181 | } 182 | if addr.To4() == nil { 183 | continue 184 | } 185 | return addr.String(), nil 186 | } 187 | } 188 | 189 | return "", fmt.Errorf("Found no IP addresses on VM") 190 | } 191 | -------------------------------------------------------------------------------- /builder/proxmox/common/client.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package proxmox 5 | 6 | import ( 7 | "crypto/tls" 8 | "log" 9 | "strings" 10 | 11 | "github.com/Telmate/proxmox-api-go/proxmox" 12 | ) 13 | 14 | func newProxmoxClient(config Config) (*proxmox.Client, error) { 15 | tlsConfig := &tls.Config{ 16 | InsecureSkipVerify: config.SkipCertValidation, 17 | } 18 | 19 | client, err := proxmox.NewClient(strings.TrimSuffix(config.proxmoxURL.String(), "/"), nil, "", tlsConfig, "", int(config.TaskTimeout.Seconds())) 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | *proxmox.Debug = config.PackerDebug 25 | 26 | if config.Token != "" { 27 | // configure token auth 28 | log.Print("using token auth") 29 | client.SetAPIToken(config.Username, config.Token) 30 | } else { 31 | // fallback to login if not using tokens 32 | log.Print("using password auth") 33 | err = client.Login(config.Username, config.Password, "") 34 | if err != nil { 35 | return nil, err 36 | } 37 | } 38 | 39 | return client, nil 40 | } 41 | -------------------------------------------------------------------------------- /builder/proxmox/common/client_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package proxmox 5 | 6 | import ( 7 | "encoding/json" 8 | "io/ioutil" 9 | "net/http" 10 | "net/http/httptest" 11 | "net/url" 12 | "testing" 13 | 14 | "github.com/Telmate/proxmox-api-go/proxmox" 15 | "github.com/stretchr/testify/require" 16 | ) 17 | 18 | func TestTokenAuth(t *testing.T) { 19 | mockAPI := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 20 | if req.Header.Get("Authorization") != "PVEAPIToken=dummy@vmhost!test-token=ac5293bf-15e2-477f-b04c-a6dfa7a46b80" { 21 | rw.WriteHeader(http.StatusUnauthorized) 22 | return 23 | } 24 | })) 25 | defer mockAPI.Close() 26 | 27 | pmURL, _ := url.Parse(mockAPI.URL) 28 | config := Config{ 29 | proxmoxURL: pmURL, 30 | SkipCertValidation: false, 31 | Username: "dummy@vmhost!test-token", 32 | Password: "not-used", 33 | Token: "ac5293bf-15e2-477f-b04c-a6dfa7a46b80", 34 | } 35 | 36 | client, err := newProxmoxClient(config) 37 | require.NoError(t, err) 38 | 39 | ref := proxmox.NewVmRef(110) 40 | ref.SetNode("node1") 41 | ref.SetVmType("qemu") 42 | err = client.Sendkey(ref, "ping") 43 | require.NoError(t, err) 44 | } 45 | 46 | func TestLogin(t *testing.T) { 47 | mockAPI := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 48 | // mock ticketing api 49 | if req.Method == http.MethodPost && req.URL.Path == "/access/ticket" { 50 | body, _ := ioutil.ReadAll(req.Body) 51 | values, _ := url.ParseQuery(string(body)) 52 | user := values.Get("username") 53 | pass := values.Get("password") 54 | if user != "dummy@vmhost" || pass != "correct-horse-battery-staple" { 55 | rw.WriteHeader(http.StatusUnauthorized) 56 | return 57 | } 58 | 59 | _ = json.NewEncoder(rw).Encode(map[string]interface{}{ 60 | "data": map[string]string{ 61 | "username": user, 62 | "ticket": "dummy-ticket", 63 | "CSRFPreventionToken": "random-token", 64 | }, 65 | }) 66 | return 67 | } 68 | 69 | // validate ticket 70 | if req.Header["Authorization"][0] != "PVEAuthCookie=dummy-ticket" { 71 | rw.WriteHeader(http.StatusUnauthorized) 72 | return 73 | } 74 | })) 75 | defer mockAPI.Close() 76 | 77 | pmURL, _ := url.Parse(mockAPI.URL) 78 | config := Config{ 79 | proxmoxURL: pmURL, 80 | SkipCertValidation: false, 81 | Username: "dummy@vmhost", 82 | Password: "correct-horse-battery-staple", 83 | Token: "", 84 | } 85 | 86 | client, err := newProxmoxClient(config) 87 | require.NoError(t, err) 88 | 89 | ref := proxmox.NewVmRef(110) 90 | ref.SetNode("node1") 91 | ref.SetVmType("qemu") 92 | err = client.Sendkey(ref, "ping") 93 | require.NoError(t, err) 94 | } 95 | -------------------------------------------------------------------------------- /builder/proxmox/common/step_convert_to_template.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package proxmox 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "log" 10 | 11 | "github.com/Telmate/proxmox-api-go/proxmox" 12 | "github.com/hashicorp/packer-plugin-sdk/multistep" 13 | packersdk "github.com/hashicorp/packer-plugin-sdk/packer" 14 | ) 15 | 16 | // stepConvertToTemplate takes the running VM configured in earlier steps, stops it, and 17 | // converts it into a Proxmox template. 18 | // 19 | // It sets the template_id state which is used for Artifact lookup. 20 | type stepConvertToTemplate struct{} 21 | 22 | type templateConverter interface { 23 | ShutdownVm(*proxmox.VmRef) (string, error) 24 | CreateTemplate(*proxmox.VmRef) error 25 | } 26 | 27 | var _ templateConverter = &proxmox.Client{} 28 | 29 | func (s *stepConvertToTemplate) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { 30 | ui := state.Get("ui").(packersdk.Ui) 31 | client := state.Get("proxmoxClient").(templateConverter) 32 | vmRef := state.Get("vmRef").(*proxmox.VmRef) 33 | 34 | ui.Say("Stopping VM") 35 | _, err := client.ShutdownVm(vmRef) 36 | if err != nil { 37 | err := fmt.Errorf("Error converting VM to template, could not stop: %s", err) 38 | state.Put("error", err) 39 | ui.Error(err.Error()) 40 | return multistep.ActionHalt 41 | } 42 | 43 | ui.Say("Converting VM to template") 44 | err = client.CreateTemplate(vmRef) 45 | if err != nil { 46 | err := fmt.Errorf("Error converting VM to template: %s", err) 47 | state.Put("error", err) 48 | ui.Error(err.Error()) 49 | return multistep.ActionHalt 50 | } 51 | log.Printf("template_id: %d", vmRef.VmId()) 52 | state.Put("template_id", vmRef.VmId()) 53 | 54 | return multistep.ActionContinue 55 | } 56 | 57 | func (s *stepConvertToTemplate) Cleanup(state multistep.StateBag) {} 58 | -------------------------------------------------------------------------------- /builder/proxmox/common/step_convert_to_template_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package proxmox 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "testing" 10 | 11 | "github.com/Telmate/proxmox-api-go/proxmox" 12 | "github.com/hashicorp/packer-plugin-sdk/multistep" 13 | packersdk "github.com/hashicorp/packer-plugin-sdk/packer" 14 | ) 15 | 16 | type converterMock struct { 17 | shutdownVm func(*proxmox.VmRef) (string, error) 18 | createTemplate func(*proxmox.VmRef) error 19 | } 20 | 21 | func (m converterMock) ShutdownVm(r *proxmox.VmRef) (string, error) { 22 | return m.shutdownVm(r) 23 | } 24 | func (m converterMock) CreateTemplate(r *proxmox.VmRef) error { 25 | return m.createTemplate(r) 26 | } 27 | 28 | var _ templateConverter = converterMock{} 29 | 30 | func TestConvertToTemplate(t *testing.T) { 31 | cs := []struct { 32 | name string 33 | shutdownErr error 34 | expectCallCreateTemplate bool 35 | createTemplateErr error 36 | expectedAction multistep.StepAction 37 | expectTemplateIdSet bool 38 | }{ 39 | { 40 | name: "no errors returns continue and sets template id", 41 | expectCallCreateTemplate: true, 42 | expectedAction: multistep.ActionContinue, 43 | expectTemplateIdSet: true, 44 | }, 45 | { 46 | name: "when shutdown fails, don't try to create template and halt", 47 | shutdownErr: fmt.Errorf("failed to stop vm"), 48 | expectCallCreateTemplate: false, 49 | expectedAction: multistep.ActionHalt, 50 | expectTemplateIdSet: false, 51 | }, 52 | { 53 | name: "when create template fails, halt", 54 | expectCallCreateTemplate: true, 55 | createTemplateErr: fmt.Errorf("failed to stop vm"), 56 | expectedAction: multistep.ActionHalt, 57 | expectTemplateIdSet: false, 58 | }, 59 | } 60 | 61 | const vmid = 123 62 | 63 | for _, c := range cs { 64 | t.Run(c.name, func(t *testing.T) { 65 | converter := converterMock{ 66 | shutdownVm: func(r *proxmox.VmRef) (string, error) { 67 | if r.VmId() != vmid { 68 | t.Errorf("ShutdownVm called with unexpected id, expected %d, got %d", vmid, r.VmId()) 69 | } 70 | return "", c.shutdownErr 71 | }, 72 | createTemplate: func(r *proxmox.VmRef) error { 73 | if r.VmId() != vmid { 74 | t.Errorf("CreateTemplate called with unexpected id, expected %d, got %d", vmid, r.VmId()) 75 | } 76 | if !c.expectCallCreateTemplate { 77 | t.Error("Did not expect CreateTemplate to be called") 78 | } 79 | 80 | return c.createTemplateErr 81 | }, 82 | } 83 | 84 | state := new(multistep.BasicStateBag) 85 | state.Put("ui", packersdk.TestUi(t)) 86 | state.Put("vmRef", proxmox.NewVmRef(vmid)) 87 | state.Put("proxmoxClient", converter) 88 | 89 | step := stepConvertToTemplate{} 90 | action := step.Run(context.TODO(), state) 91 | if action != c.expectedAction { 92 | t.Errorf("Expected action to be %v, got %v", c.expectedAction, action) 93 | } 94 | 95 | id, wasSet := state.GetOk("template_id") 96 | 97 | if c.expectTemplateIdSet != wasSet { 98 | t.Errorf("Expected template_id state present=%v was present=%v", c.expectTemplateIdSet, wasSet) 99 | } 100 | 101 | if c.expectTemplateIdSet && id != vmid { 102 | t.Errorf("Expected template_id state to be set to %d, got %v", vmid, id) 103 | } 104 | }) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /builder/proxmox/common/step_download_iso_on_pve.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package proxmox 5 | 6 | import ( 7 | "context" 8 | "encoding/hex" 9 | "fmt" 10 | "log" 11 | "path" 12 | "strings" 13 | 14 | "github.com/Telmate/proxmox-api-go/proxmox" 15 | "github.com/hashicorp/go-getter/v2" 16 | "github.com/hashicorp/packer-plugin-sdk/multistep" 17 | packersdk "github.com/hashicorp/packer-plugin-sdk/packer" 18 | ) 19 | 20 | // stepDownloadISOOnPVE downloads an ISO file directly to the specified PVE node. 21 | // Checksums are also calculated and compared on the PVE node, not by Packer. 22 | type stepDownloadISOOnPVE struct { 23 | ISO *ISOsConfig 24 | } 25 | 26 | func (s *stepDownloadISOOnPVE) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { 27 | var isoStoragePath string 28 | isoStoragePath, err := DownloadISOOnPVE(state, s.ISO.ISOUrls, s.ISO.ISOChecksum, s.ISO.ISOStoragePool) 29 | 30 | // Abort if no ISO can be downloaded 31 | if err != nil { 32 | state.Put("error", err) 33 | return multistep.ActionHalt 34 | } 35 | // If available, set the file path to the downloaded iso file on the node 36 | s.ISO.ISOFile = isoStoragePath 37 | return multistep.ActionContinue 38 | } 39 | 40 | // DownloadISOOnPVE abstracts the checksum and download process os that the code can be shared between 41 | // the common module and the iso module. This is necessary because both handle the storage path to the iso differently. 42 | // 43 | // The function takes a list of URLs to download the iso and tries them one another. 44 | // If a download was successful it skips the additonal downlaod mirrors and returns the path to the iso on the node. 45 | // 46 | // Returns: When successful, the path to the iso on the node, else an empty string. 47 | func DownloadISOOnPVE(state multistep.StateBag, ISOUrls []string, ISOChecksum string, ISOStoragePool string) (string, error) { 48 | ui := state.Get("ui").(packersdk.Ui) 49 | client := state.Get("proxmoxClient").(*proxmox.Client) 50 | c := state.Get("config").(*Config) 51 | 52 | // Generate ISOConfig configuration attributes in the format defined for packer-plugin-sdk 53 | // and use go-getter to generate parameters compatible with the Proxmox-API. 54 | var isoConfig proxmox.ConfigContent_Iso 55 | for _, url := range ISOUrls { 56 | var checksum string 57 | var checksumType string 58 | if ISOChecksum != "none" { 59 | gr := &getter.Request{ 60 | Src: url + "?checksum=" + ISOChecksum, 61 | } 62 | gc := getter.Client{} 63 | fileChecksum, err := gc.GetChecksum(context.TODO(), gr) 64 | if err != nil { 65 | state.Put("error", err) 66 | ui.Say(err.Error()) 67 | continue 68 | } 69 | checksum = hex.EncodeToString(fileChecksum.Value) 70 | checksumType = fileChecksum.Type 71 | } 72 | 73 | isoConfig = proxmox.ConfigContent_Iso{ 74 | Node: c.Node, 75 | Storage: ISOStoragePool, 76 | DownloadUrl: url, 77 | Filename: path.Base(url), 78 | ChecksumAlgorithm: checksumType, 79 | Checksum: checksum, 80 | } 81 | 82 | log.Printf("[INFO] - beginning download of %s to node %s", isoConfig.DownloadUrl, isoConfig.Node) 83 | err := proxmox.DownloadIsoFromUrl(client, isoConfig) 84 | // On error continues with the next URL and logs the error 85 | if err != nil { 86 | log.Printf("[ERROR] - failed to download iso from %s: %s", isoConfig.DownloadUrl, err) 87 | continue 88 | } 89 | isoStoragePath := fmt.Sprintf("%s:iso/%s", isoConfig.Storage, isoConfig.Filename) 90 | log.Printf("[INFO] - finished downloading %s", isoStoragePath) 91 | // Returns the path to the iso on the node 92 | return isoStoragePath, nil 93 | } 94 | 95 | // Returns an empty string, which means download was not successful. 96 | return "", fmt.Errorf("failed to download ISO with all the provided URLs, attempted: %s", strings.Join(ISOUrls, ", ")) 97 | } 98 | 99 | func (s *stepDownloadISOOnPVE) Cleanup(state multistep.StateBag) { 100 | } 101 | -------------------------------------------------------------------------------- /builder/proxmox/common/step_finalize_template_config.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package proxmox 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "regexp" 10 | "strings" 11 | 12 | "github.com/Telmate/proxmox-api-go/proxmox" 13 | "github.com/hashicorp/packer-plugin-sdk/multistep" 14 | packersdk "github.com/hashicorp/packer-plugin-sdk/packer" 15 | ) 16 | 17 | // stepFinalizeTemplateConfig does any required modifications to the configuration _after_ 18 | // the VM has been converted into a template, such as updating name and description, or 19 | // unmounting the installation ISO. 20 | type stepFinalizeTemplateConfig struct{} 21 | 22 | type templateFinalizer interface { 23 | GetVmConfig(*proxmox.VmRef) (map[string]interface{}, error) 24 | SetVmConfig(*proxmox.VmRef, map[string]interface{}) (interface{}, error) 25 | } 26 | 27 | var _ templateFinalizer = &proxmox.Client{} 28 | 29 | func (s *stepFinalizeTemplateConfig) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { 30 | ui := state.Get("ui").(packersdk.Ui) 31 | client := state.Get("proxmoxClient").(templateFinalizer) 32 | c := state.Get("config").(*Config) 33 | vmRef := state.Get("vmRef").(*proxmox.VmRef) 34 | 35 | changes := make(map[string]interface{}) 36 | 37 | changes["name"] = c.VMName 38 | if c.TemplateName != "" { 39 | changes["name"] = c.TemplateName 40 | } 41 | 42 | // During build, the description is "Packer ephemeral build VM", so if no description is 43 | // set, we need to clear it 44 | changes["description"] = c.TemplateDescription 45 | 46 | vmParams, err := client.GetVmConfig(vmRef) 47 | if err != nil { 48 | err := fmt.Errorf("error fetching template config: %s", err) 49 | state.Put("error", err) 50 | ui.Error(err.Error()) 51 | return multistep.ActionHalt 52 | } 53 | 54 | if c.CloudInit { 55 | cloudInitStoragePool := c.CloudInitStoragePool 56 | if cloudInitStoragePool == "" { 57 | if vmParams["bootdisk"] != nil && vmParams[vmParams["bootdisk"].(string)] != nil { 58 | bootDisk := vmParams[vmParams["bootdisk"].(string)].(string) 59 | cloudInitStoragePool = strings.Split(bootDisk, ":")[0] 60 | } 61 | } 62 | if cloudInitStoragePool != "" { 63 | var diskControllers []string 64 | switch c.CloudInitDiskType { 65 | // Proxmox supports up to 6 SATA controllers (0 - 5) 66 | case "sata": 67 | for i := 0; i < 6; i++ { 68 | sataController := fmt.Sprintf("sata%d", i) 69 | diskControllers = append(diskControllers, sataController) 70 | } 71 | // and up to 31 SCSI controllers (0 - 30) 72 | case "scsi": 73 | for i := 0; i < 31; i++ { 74 | scsiController := fmt.Sprintf("scsi%d", i) 75 | diskControllers = append(diskControllers, scsiController) 76 | } 77 | // Unspecified disk type defaults to "ide" 78 | case "ide": 79 | diskControllers = []string{"ide0", "ide1", "ide2", "ide3"} 80 | default: 81 | state.Put("error", fmt.Errorf("unsupported disk type %q", c.CloudInitDiskType)) 82 | return multistep.ActionHalt 83 | } 84 | cloudInitAttached := false 85 | // find a free disk controller 86 | for _, controller := range diskControllers { 87 | if vmParams[controller] == nil { 88 | ui.Say("Adding a cloud-init cdrom in storage pool " + cloudInitStoragePool) 89 | changes[controller] = cloudInitStoragePool + ":cloudinit" 90 | cloudInitAttached = true 91 | break 92 | } 93 | } 94 | if cloudInitAttached == false { 95 | err := fmt.Errorf("Found no free controller of type %s for a cloud-init cdrom", c.CloudInitDiskType) 96 | state.Put("error", err) 97 | ui.Error(err.Error()) 98 | return multistep.ActionHalt 99 | } 100 | } else { 101 | err := fmt.Errorf("cloud_init is set to true, but cloud_init_storage_pool is empty and could not be set automatically. set cloud_init_storage_pool in your configuration") 102 | state.Put("error", err) 103 | ui.Error(err.Error()) 104 | return multistep.ActionHalt 105 | } 106 | } 107 | 108 | deleteItems := []string{} 109 | if len(c.ISOs) > 0 { 110 | for idx := range c.ISOs { 111 | cdrom := c.ISOs[idx].AssignedDeviceIndex 112 | if c.ISOs[idx].Unmount { 113 | if vmParams[cdrom] == nil || !strings.Contains(vmParams[cdrom].(string), "media=cdrom") { 114 | err := fmt.Errorf("Cannot eject ISO from cdrom drive, %s is not present or not a cdrom media", cdrom) 115 | state.Put("error", err) 116 | ui.Error(err.Error()) 117 | return multistep.ActionHalt 118 | } 119 | if c.ISOs[idx].KeepCDRomDevice { 120 | changes[cdrom] = "none,media=cdrom" 121 | } else { 122 | deleteItems = append(deleteItems, cdrom) 123 | } 124 | } else { 125 | changes[cdrom] = c.ISOs[idx].ISOFile + ",media=cdrom" 126 | } 127 | } 128 | } 129 | 130 | // Disks that get replaced by the builder end up as unused disks - 131 | // find and remove them. 132 | rxUnused := regexp.MustCompile(`^unused\d+`) 133 | for key := range vmParams { 134 | if unusedDisk := rxUnused.FindString(key); unusedDisk != "" { 135 | deleteItems = append(deleteItems, unusedDisk) 136 | } 137 | } 138 | 139 | changes["delete"] = strings.Join(deleteItems, ",") 140 | 141 | if len(changes) > 0 { 142 | _, err := client.SetVmConfig(vmRef, changes) 143 | if err != nil { 144 | err := fmt.Errorf("Error updating template: %s", err) 145 | state.Put("error", err) 146 | ui.Error(err.Error()) 147 | return multistep.ActionHalt 148 | } 149 | } 150 | 151 | return multistep.ActionContinue 152 | } 153 | 154 | func (s *stepFinalizeTemplateConfig) Cleanup(state multistep.StateBag) {} 155 | -------------------------------------------------------------------------------- /builder/proxmox/common/step_finalize_template_config_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package proxmox 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "sort" 10 | "strings" 11 | "testing" 12 | 13 | "github.com/Telmate/proxmox-api-go/proxmox" 14 | "github.com/hashicorp/packer-plugin-sdk/multistep" 15 | packersdk "github.com/hashicorp/packer-plugin-sdk/packer" 16 | ) 17 | 18 | type finalizerMock struct { 19 | getConfig func() (map[string]interface{}, error) 20 | setConfig func(map[string]interface{}) (string, error) 21 | } 22 | 23 | func (m finalizerMock) GetVmConfig(*proxmox.VmRef) (map[string]interface{}, error) { 24 | return m.getConfig() 25 | } 26 | func (m finalizerMock) SetVmConfig(vmref *proxmox.VmRef, c map[string]interface{}) (interface{}, error) { 27 | return m.setConfig(c) 28 | } 29 | 30 | var _ templateFinalizer = finalizerMock{} 31 | 32 | func TestTemplateFinalize(t *testing.T) { 33 | cs := []struct { 34 | name string 35 | builderConfig *Config 36 | initialVMConfig map[string]interface{} 37 | getConfigErr error 38 | expectCallSetConfig bool 39 | expectedVMConfig map[string]interface{} 40 | setConfigErr error 41 | expectedAction multistep.StepAction 42 | expectedDelete []string 43 | }{ 44 | { 45 | name: "empty config changes name and description", 46 | builderConfig: &Config{}, 47 | initialVMConfig: map[string]interface{}{ 48 | "name": "dummy", 49 | "description": "Packer ephemeral build VM", 50 | "ide2": "local:iso/Fedora-Server-dvd-x86_64-29-1.2.iso,media=cdrom", 51 | }, 52 | expectCallSetConfig: true, 53 | expectedVMConfig: map[string]interface{}{ 54 | "name": "", 55 | "description": "", 56 | "ide2": nil, 57 | }, 58 | expectedAction: multistep.ActionContinue, 59 | }, 60 | { 61 | name: "use VM name when template name not provided", 62 | builderConfig: &Config{ 63 | VMName: "my-vm", 64 | }, 65 | initialVMConfig: map[string]interface{}{ 66 | "name": "dummy", 67 | }, 68 | expectCallSetConfig: true, 69 | expectedVMConfig: map[string]interface{}{ 70 | "name": "my-vm", 71 | }, 72 | expectedAction: multistep.ActionContinue, 73 | }, 74 | { 75 | name: "use template name when both VM name and template name are provided", 76 | builderConfig: &Config{ 77 | VMName: "my-vm", 78 | TemplateName: "my-template", 79 | }, 80 | initialVMConfig: map[string]interface{}{ 81 | "name": "dummy", 82 | }, 83 | expectCallSetConfig: true, 84 | expectedVMConfig: map[string]interface{}{ 85 | "name": "my-template", 86 | }, 87 | expectedAction: multistep.ActionContinue, 88 | }, 89 | { 90 | name: "all options", 91 | builderConfig: &Config{ 92 | TemplateName: "my-template", 93 | TemplateDescription: "some-description", 94 | }, 95 | initialVMConfig: map[string]interface{}{ 96 | "name": "dummy", 97 | "description": "Packer ephemeral build VM", 98 | }, 99 | expectCallSetConfig: true, 100 | expectedVMConfig: map[string]interface{}{ 101 | "name": "my-template", 102 | "description": "some-description", 103 | }, 104 | expectedAction: multistep.ActionContinue, 105 | }, 106 | { 107 | name: "all options with cloud-init", 108 | builderConfig: &Config{ 109 | TemplateName: "my-template", 110 | TemplateDescription: "some-description", 111 | CloudInit: true, 112 | CloudInitDiskType: "ide", 113 | }, 114 | initialVMConfig: map[string]interface{}{ 115 | "name": "dummy", 116 | "description": "Packer ephemeral build VM", 117 | "bootdisk": "virtio0", 118 | "virtio0": "ceph01:base-223-disk-0,cache=unsafe,media=disk,size=32G", 119 | }, 120 | expectCallSetConfig: true, 121 | expectedVMConfig: map[string]interface{}{ 122 | "name": "my-template", 123 | "description": "some-description", 124 | "ide0": "ceph01:cloudinit", 125 | }, 126 | expectedAction: multistep.ActionContinue, 127 | }, 128 | { 129 | name: "no available controller for cloud-init drive", 130 | builderConfig: &Config{ 131 | TemplateName: "my-template", 132 | TemplateDescription: "some-description", 133 | CloudInit: true, 134 | CloudInitDiskType: "ide", 135 | }, 136 | initialVMConfig: map[string]interface{}{ 137 | "name": "dummy", 138 | "description": "Packer ephemeral build VM", 139 | "ide0": "local:iso/Fedora-Server-dvd-x86_64-29-1.2.iso,media=cdrom", 140 | "ide1": "local:iso/Fedora-Server-dvd-x86_64-29-1.2.iso,media=cdrom", 141 | "ide2": "local:iso/Fedora-Server-dvd-x86_64-29-1.2.iso,media=cdrom", 142 | "ide3": "local:iso/Fedora-Server-dvd-x86_64-29-1.2.iso,media=cdrom", 143 | "bootdisk": "virtio0", 144 | "virtio0": "ceph01:base-223-disk-0,cache=unsafe,media=disk,size=32G", 145 | }, 146 | expectCallSetConfig: false, 147 | expectedAction: multistep.ActionHalt, 148 | }, 149 | { 150 | name: "GetVmConfig error should return halt", 151 | builderConfig: &Config{ 152 | TemplateName: "my-template", 153 | TemplateDescription: "some-description", 154 | CloudInit: true, 155 | CloudInitDiskType: "ide", 156 | }, 157 | getConfigErr: fmt.Errorf("some error"), 158 | expectCallSetConfig: false, 159 | expectedAction: multistep.ActionHalt, 160 | }, 161 | { 162 | name: "SetVmConfig error should return halt", 163 | builderConfig: &Config{ 164 | TemplateName: "my-template", 165 | TemplateDescription: "some-description", 166 | }, 167 | initialVMConfig: map[string]interface{}{ 168 | "name": "dummy", 169 | "description": "Packer ephemeral build VM", 170 | }, 171 | expectCallSetConfig: true, 172 | setConfigErr: fmt.Errorf("some error"), 173 | expectedAction: multistep.ActionHalt, 174 | }, 175 | { 176 | name: "find and remove unused disks", 177 | builderConfig: &Config{}, 178 | initialVMConfig: map[string]interface{}{ 179 | "unused0": "local-zfs:vm-110-disk-1", 180 | "unused99": "local-zfs:vm-110-disk-100", 181 | }, 182 | expectCallSetConfig: true, 183 | expectedDelete: []string{"unused0", "unused99"}, 184 | expectedAction: multistep.ActionContinue, 185 | }, 186 | } 187 | 188 | for _, c := range cs { 189 | t.Run(c.name, func(t *testing.T) { 190 | finalizer := finalizerMock{ 191 | getConfig: func() (map[string]interface{}, error) { 192 | return c.initialVMConfig, c.getConfigErr 193 | }, 194 | setConfig: func(cfg map[string]interface{}) (string, error) { 195 | if !c.expectCallSetConfig { 196 | t.Error("Did not expect SetVmConfig to be called") 197 | } 198 | for key, val := range c.expectedVMConfig { 199 | if cfg[key] != val { 200 | t.Errorf("Expected %q to be %q, got %q", key, val, cfg[key]) 201 | } 202 | } 203 | // We need to sort deletes first, to test them reliably 204 | var gotDelete = strings.Split(cfg["delete"].(string), ",") 205 | sort.Strings(gotDelete) 206 | sort.Strings(c.expectedDelete) 207 | if strings.Join(gotDelete, ",") != strings.Join(c.expectedDelete, ",") { 208 | t.Errorf("Expected delete to be %s, got %s", c.expectedDelete, cfg["delete"]) 209 | } 210 | 211 | return "", c.setConfigErr 212 | }, 213 | } 214 | 215 | state := new(multistep.BasicStateBag) 216 | state.Put("ui", packersdk.TestUi(t)) 217 | state.Put("config", c.builderConfig) 218 | state.Put("vmRef", proxmox.NewVmRef(1)) 219 | state.Put("proxmoxClient", finalizer) 220 | 221 | step := stepFinalizeTemplateConfig{} 222 | action := step.Run(context.TODO(), state) 223 | if action != c.expectedAction { 224 | t.Errorf("Expected action to be %v, got %v", c.expectedAction, action) 225 | } 226 | }) 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /builder/proxmox/common/step_remove_cloud_init_drive.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package proxmox 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "strings" 10 | 11 | "github.com/Telmate/proxmox-api-go/proxmox" 12 | "github.com/hashicorp/packer-plugin-sdk/multistep" 13 | packersdk "github.com/hashicorp/packer-plugin-sdk/packer" 14 | ) 15 | 16 | // stepRemoveCloudInitDrive removes the cloud-init cdrom and deletes 17 | // VM config parameters related to cloud-init. 18 | type stepRemoveCloudInitDrive struct{} 19 | 20 | type CloudInitDriveRemover interface { 21 | GetVmConfig(*proxmox.VmRef) (map[string]interface{}, error) 22 | SetVmConfig(*proxmox.VmRef, map[string]interface{}) (interface{}, error) 23 | } 24 | 25 | var _ CloudInitDriveRemover = &proxmox.Client{} 26 | 27 | func (s *stepRemoveCloudInitDrive) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { 28 | ui := state.Get("ui").(packersdk.Ui) 29 | client := state.Get("proxmoxClient").(CloudInitDriveRemover) 30 | vmRef := state.Get("vmRef").(*proxmox.VmRef) 31 | 32 | changes := make(map[string]interface{}) 33 | delete := []string{} 34 | 35 | vmParams, err := client.GetVmConfig(vmRef) 36 | if err != nil { 37 | err := fmt.Errorf("error fetching template config: %s", err) 38 | state.Put("error", err) 39 | ui.Error(err.Error()) 40 | return multistep.ActionHalt 41 | } 42 | // The cloud-init drive is usually attached to ide0, but check the other controllers as well 43 | diskControllers := []string{"ide0", "ide1", "ide2", "ide3"} 44 | // Proxmox supports up to 6 SATA controllers (0 - 5) 45 | for i := 0; i < 6; i++ { 46 | sataController := fmt.Sprintf("sata%d", i) 47 | diskControllers = append(diskControllers, sataController) 48 | } 49 | // and up to 31 SCSI controllers (0 - 30) 50 | for i := 0; i < 31; i++ { 51 | scsiController := fmt.Sprintf("scsi%d", i) 52 | diskControllers = append(diskControllers, scsiController) 53 | } 54 | 55 | for _, controller := range diskControllers { 56 | if vmParams[controller] != nil && strings.Contains(vmParams[controller].(string), "-cloudinit") && strings.Contains(vmParams[controller].(string), ",media=cdrom") { 57 | delete = append(delete, controller) 58 | } 59 | } 60 | 61 | CloudInitParameters := []string{ 62 | "cipassword", 63 | "ciuser", 64 | "nameserver", 65 | "searchdomain", 66 | "sshkeys", 67 | } 68 | for i := 0; i < 16; i++ { 69 | ipconfig := fmt.Sprintf("ipconfig%d", i) 70 | CloudInitParameters = append(CloudInitParameters, ipconfig) 71 | } 72 | for _, parameter := range CloudInitParameters { 73 | if vmParams[parameter] != nil { 74 | delete = append(delete, parameter) 75 | } 76 | } 77 | 78 | if len(delete) > 0 { 79 | changes["delete"] = strings.Join(delete, ",") 80 | 81 | _, err := client.SetVmConfig(vmRef, changes) 82 | if err != nil { 83 | err := fmt.Errorf("error updating template: %s", err) 84 | state.Put("error", err) 85 | ui.Error(err.Error()) 86 | return multistep.ActionHalt 87 | } 88 | } 89 | 90 | return multistep.ActionContinue 91 | } 92 | 93 | func (s *stepRemoveCloudInitDrive) Cleanup(state multistep.StateBag) { 94 | } 95 | -------------------------------------------------------------------------------- /builder/proxmox/common/step_remove_cloud_init_drive_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package proxmox 5 | 6 | import ( 7 | "context" 8 | "testing" 9 | 10 | "github.com/Telmate/proxmox-api-go/proxmox" 11 | "github.com/hashicorp/packer-plugin-sdk/multistep" 12 | packersdk "github.com/hashicorp/packer-plugin-sdk/packer" 13 | ) 14 | 15 | type CloudInitDriveRemoverMock struct { 16 | getConfig func() (map[string]interface{}, error) 17 | setConfig func(map[string]interface{}) (string, error) 18 | } 19 | 20 | func (m CloudInitDriveRemoverMock) GetVmConfig(*proxmox.VmRef) (map[string]interface{}, error) { 21 | return m.getConfig() 22 | } 23 | func (m CloudInitDriveRemoverMock) SetVmConfig(vmref *proxmox.VmRef, c map[string]interface{}) (interface{}, error) { 24 | return m.setConfig(c) 25 | } 26 | 27 | var _ CloudInitDriveRemover = CloudInitDriveRemoverMock{} 28 | 29 | func TestRemoveCloudInitDrive(t *testing.T) { 30 | cs := []struct { 31 | name string 32 | builderConfig *Config 33 | initialVMConfig map[string]interface{} 34 | getConfigErr error 35 | expectCallSetConfig bool 36 | expectedDelete string 37 | setConfigErr error 38 | expectedAction multistep.StepAction 39 | }{ 40 | { 41 | name: "remove Cloud-Init drive", 42 | builderConfig: &Config{}, 43 | initialVMConfig: map[string]interface{}{ 44 | "ide3": "local-zfs:vm-500-cloudinit,media=cdrom", 45 | }, 46 | expectCallSetConfig: true, 47 | expectedDelete: "ide3", 48 | expectedAction: multistep.ActionContinue, 49 | }, 50 | { 51 | name: "leave other drives alone", 52 | builderConfig: &Config{}, 53 | initialVMConfig: map[string]interface{}{ 54 | "ide2": "local:iso/debian-11.6.0-amd64-netinst.iso,media=cdrom,size=388M", 55 | "scsi0": "local-zfs:vm-108-disk-0,iothread=1,size=10G", 56 | }, 57 | expectCallSetConfig: false, 58 | expectedDelete: "", 59 | expectedAction: multistep.ActionContinue, 60 | }, 61 | { 62 | name: "find Cloud-Init drive on all controllers", 63 | builderConfig: &Config{}, 64 | initialVMConfig: map[string]interface{}{ 65 | "ide3": "local-zfs:vm-104-cloudinit,media=cdrom,size=4M", 66 | "sata5": "local-zfs:vm-104-cloudinit,media=cdrom,size=4M", 67 | "scsi30": "local-zfs:vm-104-cloudinit,media=cdrom,size=4M", 68 | }, 69 | expectCallSetConfig: true, 70 | expectedDelete: "ide3,sata5,scsi30", 71 | expectedAction: multistep.ActionContinue, 72 | }, 73 | { 74 | name: "remove cloud-init config options", 75 | builderConfig: &Config{}, 76 | initialVMConfig: map[string]interface{}{ 77 | "ciuser": "root", 78 | "cipassword": "$5$FUaXk2yX$2/E0AXhEfGJqxNA6H9ORURyS8WSnsm8uT9S4vvDqwd7", 79 | "sshkeys": "ssh-ed25519%20AAAAC3NzaC1lZDI1NTE5AAAAIEGL6AvL7oe7nQThd8hr6%2FqKeYtQv3oG%2Fur1x2U3AovJ%20packer", 80 | "searchdomain": "example.com", 81 | "nameserver": "9.9.9.9 149.112.112.112", 82 | "ipconfig0": "ip=10.0.10.123/24,gw=10.0.10.1", 83 | }, 84 | expectCallSetConfig: true, 85 | expectedDelete: "cipassword,ciuser,nameserver,searchdomain,sshkeys,ipconfig0", 86 | expectedAction: multistep.ActionContinue, 87 | }, 88 | } 89 | 90 | for _, c := range cs { 91 | t.Run(c.name, func(t *testing.T) { 92 | remover := CloudInitDriveRemoverMock{ 93 | getConfig: func() (map[string]interface{}, error) { 94 | return c.initialVMConfig, c.getConfigErr 95 | }, 96 | setConfig: func(changes map[string]interface{}) (string, error) { 97 | if !c.expectCallSetConfig { 98 | t.Error("Did not expect SetVmConfig to be called") 99 | } 100 | if changes["delete"] != c.expectedDelete { 101 | t.Errorf("Expected delete to be %s, got %s", c.expectedDelete, changes["delete"]) 102 | } 103 | 104 | return "", c.setConfigErr 105 | }, 106 | } 107 | 108 | state := new(multistep.BasicStateBag) 109 | state.Put("ui", packersdk.TestUi(t)) 110 | state.Put("config", c.builderConfig) 111 | state.Put("vmRef", proxmox.NewVmRef(1)) 112 | state.Put("proxmoxClient", remover) 113 | 114 | step := stepRemoveCloudInitDrive{} 115 | action := step.Run(context.TODO(), state) 116 | if action != c.expectedAction { 117 | t.Errorf("Expected action to be %v, got %v", c.expectedAction, action) 118 | } 119 | }) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /builder/proxmox/common/step_success.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package proxmox 5 | 6 | import ( 7 | "context" 8 | 9 | "github.com/hashicorp/packer-plugin-sdk/multistep" 10 | ) 11 | 12 | // stepSuccess runs after the full build has succeeded. 13 | // 14 | // It sets the success state, which ensures cleanup does not remove the finished template 15 | type stepSuccess struct{} 16 | 17 | func (s *stepSuccess) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { 18 | // We need to ensure stepStartVM.Cleanup doesn't delete the template (no 19 | // difference between VMs and templates when deleting) 20 | state.Put("success", true) 21 | 22 | return multistep.ActionContinue 23 | } 24 | 25 | func (s *stepSuccess) Cleanup(state multistep.StateBag) {} 26 | -------------------------------------------------------------------------------- /builder/proxmox/common/step_type_boot_command.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package proxmox 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | "fmt" 10 | "log" 11 | "net" 12 | "time" 13 | 14 | "github.com/Telmate/proxmox-api-go/proxmox" 15 | "github.com/hashicorp/packer-plugin-sdk/bootcommand" 16 | "github.com/hashicorp/packer-plugin-sdk/multistep" 17 | packersdk "github.com/hashicorp/packer-plugin-sdk/packer" 18 | "github.com/hashicorp/packer-plugin-sdk/template/interpolate" 19 | ) 20 | 21 | // stepTypeBootCommand takes the started VM, and sends the keystrokes required to start 22 | // the installation process such that Packer can later reach the VM over SSH/WinRM 23 | type stepTypeBootCommand struct { 24 | bootcommand.BootConfig 25 | Ctx interpolate.Context 26 | } 27 | 28 | type bootCommandTemplateData struct { 29 | HTTPIP string 30 | HTTPPort int 31 | } 32 | 33 | type commandTyper interface { 34 | Sendkey(*proxmox.VmRef, string) error 35 | } 36 | 37 | var _ commandTyper = &proxmox.Client{} 38 | 39 | func (s *stepTypeBootCommand) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { 40 | ui := state.Get("ui").(packersdk.Ui) 41 | c := state.Get("config").(*Config) 42 | client := state.Get("proxmoxClient").(commandTyper) 43 | vmRef := state.Get("vmRef").(*proxmox.VmRef) 44 | 45 | if len(s.BootCommand) == 0 { 46 | log.Println("No boot command given, skipping") 47 | return multistep.ActionContinue 48 | } 49 | 50 | if int64(s.BootWait) > 0 { 51 | ui.Say(fmt.Sprintf("Waiting %s for boot", s.BootWait)) 52 | select { 53 | case <-time.After(s.BootWait): 54 | break 55 | case <-ctx.Done(): 56 | return multistep.ActionHalt 57 | } 58 | } 59 | var httpIP string 60 | var err error 61 | if c.HTTPAddress != "0.0.0.0" { 62 | httpIP = c.HTTPAddress 63 | } else { 64 | httpIP, err = hostIP(c.HTTPInterface) 65 | if err != nil { 66 | err := fmt.Errorf("Failed to determine host IP: %s", err) 67 | state.Put("error", err) 68 | ui.Error(err.Error()) 69 | return multistep.ActionHalt 70 | } 71 | } 72 | 73 | state.Put("http_ip", httpIP) 74 | s.Ctx.Data = &bootCommandTemplateData{ 75 | HTTPIP: httpIP, 76 | HTTPPort: state.Get("http_port").(int), 77 | } 78 | 79 | ui.Say("Typing the boot command") 80 | d := NewProxmoxDriver(client, vmRef, c.BootKeyInterval) 81 | command, err := interpolate.Render(s.FlatBootCommand(), &s.Ctx) 82 | if err != nil { 83 | err := fmt.Errorf("Error preparing boot command: %s", err) 84 | state.Put("error", err) 85 | ui.Error(err.Error()) 86 | return multistep.ActionHalt 87 | } 88 | 89 | seq, err := bootcommand.GenerateExpressionSequence(command) 90 | if err != nil { 91 | err := fmt.Errorf("Error generating boot command: %s", err) 92 | state.Put("error", err) 93 | ui.Error(err.Error()) 94 | return multistep.ActionHalt 95 | } 96 | 97 | if err := seq.Do(ctx, d); err != nil { 98 | err := fmt.Errorf("Error running boot command: %s", err) 99 | state.Put("error", err) 100 | ui.Error(err.Error()) 101 | return multistep.ActionHalt 102 | } 103 | 104 | return multistep.ActionContinue 105 | } 106 | 107 | func (*stepTypeBootCommand) Cleanup(multistep.StateBag) {} 108 | 109 | func hostIP(ifname string) (string, error) { 110 | var addrs []net.Addr 111 | var err error 112 | 113 | if ifname != "" { 114 | iface, err := net.InterfaceByName(ifname) 115 | if err != nil { 116 | return "", err 117 | } 118 | addrs, err = iface.Addrs() 119 | if err != nil { 120 | return "", err 121 | } 122 | } else { 123 | addrs, err = net.InterfaceAddrs() 124 | if err != nil { 125 | return "", err 126 | } 127 | } 128 | for _, addr := range addrs { 129 | if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { 130 | if ipnet.IP.To4() != nil { 131 | return ipnet.IP.String(), nil 132 | } 133 | } 134 | } 135 | return "", errors.New("No host IP found") 136 | } 137 | -------------------------------------------------------------------------------- /builder/proxmox/common/step_type_boot_command_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package proxmox 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "strings" 10 | "testing" 11 | 12 | "github.com/Telmate/proxmox-api-go/proxmox" 13 | "github.com/hashicorp/packer-plugin-sdk/bootcommand" 14 | "github.com/hashicorp/packer-plugin-sdk/multistep" 15 | packersdk "github.com/hashicorp/packer-plugin-sdk/packer" 16 | ) 17 | 18 | type commandTyperMock struct { 19 | sendkey func(*proxmox.VmRef, string) error 20 | } 21 | 22 | func (m commandTyperMock) Sendkey(ref *proxmox.VmRef, cmd string) error { 23 | return m.sendkey(ref, cmd) 24 | } 25 | 26 | var _ commandTyper = commandTyperMock{} 27 | 28 | func TestTypeBootCommand(t *testing.T) { 29 | cs := []struct { 30 | name string 31 | builderConfig *Config 32 | expectCallSendkey bool 33 | sendkeyErr error 34 | expectedKeysSent string 35 | expectedAction multistep.StepAction 36 | }{ 37 | { 38 | name: "simple boot command is typed", 39 | builderConfig: &Config{BootConfig: bootcommand.BootConfig{BootCommand: []string{"hello"}}}, 40 | expectCallSendkey: true, 41 | expectedKeysSent: "hello", 42 | expectedAction: multistep.ActionContinue, 43 | }, 44 | { 45 | name: "interpolated boot command", 46 | builderConfig: &Config{BootConfig: bootcommand.BootConfig{BootCommand: []string{"helloworld"}}}, 47 | expectCallSendkey: true, 48 | expectedKeysSent: "helloretworld", 49 | expectedAction: multistep.ActionContinue, 50 | }, 51 | { 52 | name: "merge multiple interpolated boot command", 53 | builderConfig: &Config{BootConfig: bootcommand.BootConfig{BootCommand: []string{"Hello World 2.0", "foo!bar@baz"}}}, 54 | expectCallSendkey: true, 55 | expectedKeysSent: "shift-hellospcshift-worldspc2dot0fooshift-1barshift-2baz", 56 | expectedAction: multistep.ActionContinue, 57 | }, 58 | { 59 | name: "holding and releasing keys", 60 | builderConfig: &Config{BootConfig: bootcommand.BootConfig{BootCommand: []string{"helloworld"}}}, 61 | expectCallSendkey: true, 62 | expectedKeysSent: "shift-hshift-eshift-lshift-lshift-oshift-alt_r-wshift-alt_r-oshift-alt_r-rshift-alt_r-lshift-alt_r-d", 63 | expectedAction: multistep.ActionContinue, 64 | }, 65 | { 66 | name: "holding multiple alphabetical keys and shift", 67 | builderConfig: &Config{BootConfig: bootcommand.BootConfig{BootCommand: []string{"n"}}}, 68 | expectCallSendkey: true, 69 | expectedKeysSent: "shift-c-n", 70 | expectedAction: multistep.ActionContinue, 71 | }, 72 | { 73 | name: "noop keystrokes", 74 | builderConfig: &Config{BootConfig: bootcommand.BootConfig{BootCommand: []string{""}}}, 75 | expectCallSendkey: true, 76 | expectedKeysSent: "", 77 | expectedAction: multistep.ActionContinue, 78 | }, 79 | { 80 | name: "noop keystrokes mixed", 81 | builderConfig: &Config{BootConfig: bootcommand.BootConfig{BootCommand: []string{"h"}}}, 82 | expectCallSendkey: true, 83 | expectedKeysSent: "shift-h", 84 | expectedAction: multistep.ActionContinue, 85 | }, 86 | { 87 | name: "without boot command sendkey should not be called", 88 | builderConfig: &Config{BootConfig: bootcommand.BootConfig{BootCommand: []string{}}}, 89 | expectCallSendkey: false, 90 | expectedAction: multistep.ActionContinue, 91 | }, 92 | { 93 | name: "invalid boot command template function", 94 | builderConfig: &Config{BootConfig: bootcommand.BootConfig{BootCommand: []string{"{{ foo }}"}}}, 95 | expectCallSendkey: false, 96 | expectedAction: multistep.ActionHalt, 97 | }, 98 | { 99 | // When proxmox (or Qemu, really) doesn't recognize the keycode we send, we get no error back, but 100 | // a map {"data": "invalid parameter: X"}, where X is the keycode. 101 | name: "invalid keys sent to proxmox", 102 | builderConfig: &Config{BootConfig: bootcommand.BootConfig{BootCommand: []string{"x"}}}, 103 | expectCallSendkey: true, 104 | sendkeyErr: fmt.Errorf("invalid parameter: x"), 105 | expectedKeysSent: "x", 106 | expectedAction: multistep.ActionHalt, 107 | }, 108 | { 109 | name: "error in typing should return halt", 110 | builderConfig: &Config{BootConfig: bootcommand.BootConfig{BootCommand: []string{"hello"}}}, 111 | expectCallSendkey: true, 112 | sendkeyErr: fmt.Errorf("some error"), 113 | expectedKeysSent: "h", 114 | expectedAction: multistep.ActionHalt, 115 | }, 116 | } 117 | 118 | for _, c := range cs { 119 | t.Run(c.name, func(t *testing.T) { 120 | accumulator := strings.Builder{} 121 | typer := commandTyperMock{ 122 | sendkey: func(ref *proxmox.VmRef, cmd string) error { 123 | if !c.expectCallSendkey { 124 | t.Error("Did not expect sendkey to be called") 125 | } 126 | 127 | accumulator.WriteString(cmd) 128 | 129 | return c.sendkeyErr 130 | }, 131 | } 132 | 133 | state := new(multistep.BasicStateBag) 134 | state.Put("ui", packersdk.TestUi(t)) 135 | state.Put("config", c.builderConfig) 136 | state.Put("http_port", int(0)) 137 | state.Put("vmRef", proxmox.NewVmRef(1)) 138 | state.Put("proxmoxClient", typer) 139 | 140 | step := stepTypeBootCommand{ 141 | c.builderConfig.BootConfig, 142 | c.builderConfig.Ctx, 143 | } 144 | action := step.Run(context.TODO(), state) 145 | step.Cleanup(state) 146 | 147 | if action != c.expectedAction { 148 | t.Errorf("Expected action to be %v, got %v", c.expectedAction, action) 149 | } 150 | if c.expectedKeysSent != accumulator.String() { 151 | t.Errorf("Expected keystrokes to be %q, got %q", c.expectedKeysSent, accumulator.String()) 152 | } 153 | }) 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /builder/proxmox/common/step_upload_iso.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package proxmox 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "io" 10 | "os" 11 | "path/filepath" 12 | 13 | proxmoxapi "github.com/Telmate/proxmox-api-go/proxmox" 14 | "github.com/hashicorp/packer-plugin-sdk/multistep" 15 | packersdk "github.com/hashicorp/packer-plugin-sdk/packer" 16 | ) 17 | 18 | // stepUploadISO uploads an ISO file 19 | type stepUploadISO struct { 20 | ISO *ISOsConfig 21 | } 22 | 23 | type uploader interface { 24 | Upload(node string, storage string, contentType string, filename string, file io.Reader) error 25 | DeleteVolume(vmr *proxmoxapi.VmRef, storageName string, volumeName string) (exitStatus interface{}, err error) 26 | } 27 | 28 | var _ uploader = &proxmoxapi.Client{} 29 | 30 | func (s *stepUploadISO) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { 31 | ui := state.Get("ui").(packersdk.Ui) 32 | client := state.Get("proxmoxClient").(uploader) 33 | c := state.Get("config").(*Config) 34 | 35 | if !s.ISO.ShouldUploadISO { 36 | return multistep.ActionContinue 37 | } 38 | 39 | if len(s.ISO.CDFiles) > 0 || len(s.ISO.CDContent) > 0 { 40 | // output from commonsteps.StepCreateCD should have populate cd_path 41 | if cdPath, ok := state.GetOk("cd_path"); ok { 42 | state.Put(s.ISO.DownloadPathKey, cdPath.(string)) 43 | state.Remove("cd_path") 44 | } else { 45 | err := fmt.Errorf("expected cd_path from commonsteps.StepCreateCD to be set") 46 | state.Put("error", err) 47 | ui.Error(err.Error()) 48 | return multistep.ActionHalt 49 | } 50 | } 51 | 52 | p := state.Get(s.ISO.DownloadPathKey).(string) 53 | if p == "" { 54 | err := fmt.Errorf("path to downloaded ISO was empty") 55 | state.Put("error", err) 56 | ui.Error(err.Error()) 57 | return multistep.ActionHalt 58 | } 59 | 60 | isoPath, err := filepath.EvalSymlinks(p) 61 | if err != nil { 62 | state.Put("error", err) 63 | ui.Error(err.Error()) 64 | return multistep.ActionHalt 65 | } 66 | 67 | r, err := os.Open(isoPath) 68 | if err != nil { 69 | state.Put("error", err) 70 | ui.Error(err.Error()) 71 | return multistep.ActionHalt 72 | } 73 | 74 | filename := filepath.Base(isoPath) 75 | err = client.Upload(c.Node, s.ISO.ISOStoragePool, "iso", filename, r) 76 | if err != nil { 77 | state.Put("error", err) 78 | ui.Error(err.Error()) 79 | return multistep.ActionHalt 80 | } 81 | 82 | isoStoragePath := fmt.Sprintf("%s:iso/%s", s.ISO.ISOStoragePool, filename) 83 | s.ISO.ISOFile = isoStoragePath 84 | ui.Message(fmt.Sprintf("Uploaded ISO to %s", isoStoragePath)) 85 | 86 | return multistep.ActionContinue 87 | } 88 | 89 | func (s *stepUploadISO) Cleanup(state multistep.StateBag) { 90 | c := state.Get("config").(*Config) 91 | ui := state.Get("ui").(packersdk.Ui) 92 | client := state.Get("proxmoxClient").(uploader) 93 | 94 | // If everything finished successfully and we want to keep the ISO mounted, don't cleanup generated ISO 95 | if _, ok := state.GetOk("success"); ok && !s.ISO.Unmount { 96 | return 97 | } 98 | 99 | if (len(s.ISO.CDFiles) > 0 || len(s.ISO.CDContent) > 0) && s.ISO.DownloadPathKey != "" { 100 | // Fake a VM reference, DeleteVolume just needs the node to be valid 101 | vmRef := &proxmoxapi.VmRef{} 102 | vmRef.SetNode(c.Node) 103 | vmRef.SetVmType("qemu") 104 | 105 | _, err := client.DeleteVolume(vmRef, s.ISO.ISOStoragePool, s.ISO.ISOFile) 106 | if err != nil { 107 | state.Put("error", err) 108 | ui.Error(fmt.Sprintf("delete volume failed: %s", err.Error())) 109 | return 110 | } 111 | ui.Message(fmt.Sprintf("Deleted generated ISO from %s", s.ISO.ISOFile)) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /builder/proxmox/common/step_upload_iso_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package proxmox 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "io" 10 | "testing" 11 | 12 | "github.com/Telmate/proxmox-api-go/proxmox" 13 | "github.com/hashicorp/packer-plugin-sdk/multistep" 14 | "github.com/hashicorp/packer-plugin-sdk/multistep/commonsteps" 15 | packersdk "github.com/hashicorp/packer-plugin-sdk/packer" 16 | ) 17 | 18 | type uploaderMock struct { 19 | uploadFail bool 20 | deleteFail bool 21 | uploadWasCalled bool 22 | deleteWasCalled bool 23 | } 24 | 25 | func (m *uploaderMock) Upload(node string, storage string, contentType string, filename string, file io.Reader) error { 26 | m.uploadWasCalled = true 27 | if m.uploadFail { 28 | return fmt.Errorf("Testing induced Upload failure") 29 | } 30 | return nil 31 | } 32 | 33 | func (m *uploaderMock) DeleteVolume(vmr *proxmox.VmRef, storageName string, volumeName string) (exitStatus interface{}, err error) { 34 | m.deleteWasCalled = true 35 | if m.deleteFail { 36 | return nil, fmt.Errorf("Testing induced DeleteVolume failure") 37 | } 38 | return 39 | } 40 | 41 | var _ uploader = &uploaderMock{} 42 | 43 | func TestUploadISO(t *testing.T) { 44 | cs := []struct { 45 | name string 46 | builderConfig *Config 47 | step *stepUploadISO 48 | testAssert func(m *uploaderMock, action multistep.StepAction) 49 | downloadPath string 50 | generatedISOPath string 51 | failUpload bool 52 | failDelete bool 53 | expectError bool 54 | expectUploadCalled bool 55 | expectDeleteCalled bool 56 | expectedISOPath string 57 | expectedAction multistep.StepAction 58 | }{ 59 | { 60 | name: "should not call upload unless configured to do so", 61 | builderConfig: &Config{}, 62 | step: &stepUploadISO{ 63 | ISO: &ISOsConfig{ 64 | ShouldUploadISO: false, 65 | }, 66 | }, 67 | expectError: false, 68 | expectedAction: multistep.ActionContinue, 69 | }, 70 | { 71 | name: "StepCreateCD not called (no cd_path present) should halt", 72 | builderConfig: &Config{}, 73 | step: &stepUploadISO{ 74 | ISO: &ISOsConfig{ 75 | ShouldUploadISO: true, 76 | CDConfig: commonsteps.CDConfig{ 77 | CDFiles: []string{"testfile"}, 78 | }, 79 | }, 80 | }, 81 | expectError: true, 82 | expectedAction: multistep.ActionHalt, 83 | }, 84 | { 85 | name: "DownloadPathKey not valid should halt", 86 | builderConfig: &Config{}, 87 | step: &stepUploadISO{ 88 | ISO: &ISOsConfig{ 89 | ShouldUploadISO: true, 90 | DownloadPathKey: "", 91 | }, 92 | }, 93 | expectError: true, 94 | expectedAction: multistep.ActionHalt, 95 | }, 96 | { 97 | name: "ISO not found should halt", 98 | builderConfig: &Config{}, 99 | step: &stepUploadISO{ 100 | ISO: &ISOsConfig{ 101 | ShouldUploadISO: true, 102 | DownloadPathKey: "filethatdoesnotexist.iso", 103 | }, 104 | }, 105 | downloadPath: "filethatdoesnotexist.iso", 106 | expectError: true, 107 | expectedAction: multistep.ActionHalt, 108 | }, 109 | { 110 | name: "generated ISO should be uploaded and deleted", 111 | builderConfig: &Config{}, 112 | step: &stepUploadISO{ 113 | ISO: &ISOsConfig{ 114 | ShouldUploadISO: true, 115 | ISOStoragePool: "local", 116 | CDConfig: commonsteps.CDConfig{ 117 | CDFiles: []string{"testfile"}, 118 | }, 119 | DownloadPathKey: "../iso/testdata/test.iso", 120 | }, 121 | }, 122 | generatedISOPath: "../iso/testdata/test.iso", 123 | 124 | expectError: false, 125 | expectedAction: multistep.ActionContinue, 126 | expectUploadCalled: true, 127 | expectedISOPath: "local:iso/test.iso", 128 | expectDeleteCalled: true, 129 | }, 130 | { 131 | name: "generated ISO should be uploaded but deletion failed", 132 | builderConfig: &Config{}, 133 | step: &stepUploadISO{ 134 | ISO: &ISOsConfig{ 135 | ShouldUploadISO: true, 136 | ISOStoragePool: "local", 137 | CDConfig: commonsteps.CDConfig{ 138 | CDFiles: []string{"testfile"}, 139 | }, 140 | DownloadPathKey: "../iso/testdata/test.iso", 141 | }, 142 | }, 143 | generatedISOPath: "../iso/testdata/test.iso", 144 | failDelete: true, 145 | expectError: true, 146 | expectedAction: multistep.ActionContinue, 147 | expectUploadCalled: true, 148 | expectedISOPath: "local:iso/test.iso", 149 | expectDeleteCalled: true, 150 | }, 151 | { 152 | name: "downloaded ISO should be uploaded", 153 | builderConfig: &Config{}, 154 | step: &stepUploadISO{ 155 | ISO: &ISOsConfig{ 156 | ShouldUploadISO: true, 157 | ISOStoragePool: "local", 158 | DownloadPathKey: "../iso/testdata/test.iso", 159 | }, 160 | }, 161 | downloadPath: "../iso/testdata/test.iso", 162 | 163 | expectError: false, 164 | expectedAction: multistep.ActionContinue, 165 | expectUploadCalled: true, 166 | expectedISOPath: "local:iso/test.iso", 167 | expectDeleteCalled: false, 168 | }, 169 | { 170 | name: "downloaded ISO fail upload", 171 | builderConfig: &Config{}, 172 | step: &stepUploadISO{ 173 | ISO: &ISOsConfig{ 174 | ShouldUploadISO: true, 175 | ISOStoragePool: "local", 176 | DownloadPathKey: "../iso/testdata/test.iso", 177 | }, 178 | }, 179 | downloadPath: "../iso/testdata/test.iso", 180 | failUpload: true, 181 | expectError: true, 182 | expectedAction: multistep.ActionHalt, 183 | expectUploadCalled: true, 184 | expectDeleteCalled: false, 185 | }, 186 | } 187 | 188 | for _, c := range cs { 189 | t.Run(c.name, func(t *testing.T) { 190 | m := &uploaderMock{uploadFail: c.failUpload, deleteFail: c.failDelete} 191 | 192 | state := new(multistep.BasicStateBag) 193 | state.Put("ui", packersdk.TestUi(t)) 194 | state.Put("config", c.builderConfig) 195 | state.Put("proxmoxClient", m) 196 | state.Put(c.step.ISO.DownloadPathKey, c.downloadPath) 197 | state.Put("cd_path", c.generatedISOPath) 198 | 199 | step := c.step 200 | action := step.Run(context.TODO(), state) 201 | step.Cleanup(state) 202 | 203 | if action != c.expectedAction { 204 | t.Errorf("Expected action to be %v, got %v", c.expectedAction, action) 205 | } 206 | if m.uploadWasCalled != c.expectUploadCalled { 207 | t.Errorf("Expected mock upload to be called: %v, got: %v", c.expectUploadCalled, m.uploadWasCalled) 208 | } 209 | if m.deleteWasCalled != c.expectDeleteCalled { 210 | t.Errorf("Expected mock delete to be called: %v, got: %v", c.expectDeleteCalled, m.deleteWasCalled) 211 | } 212 | err, gotError := state.GetOk("error") 213 | if gotError != c.expectError { 214 | t.Errorf("Expected error state to be: %v, got: %v", c.expectError, gotError) 215 | } 216 | if err == nil { 217 | if c.step.ISO.ISOFile != c.expectedISOPath { 218 | t.Errorf("Expected state iso_path to be %q, got %q", c.expectedISOPath, c.step.ISO.ISOFile) 219 | } 220 | } 221 | }) 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /builder/proxmox/iso/builder.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package proxmoxiso 5 | 6 | import ( 7 | "context" 8 | 9 | proxmoxapi "github.com/Telmate/proxmox-api-go/proxmox" 10 | "github.com/hashicorp/hcl/v2/hcldec" 11 | proxmox "github.com/hashicorp/packer-plugin-proxmox/builder/proxmox/common" 12 | "github.com/hashicorp/packer-plugin-sdk/multistep" 13 | packersdk "github.com/hashicorp/packer-plugin-sdk/packer" 14 | ) 15 | 16 | // The unique id for the builder 17 | const BuilderID = "proxmox.iso" 18 | 19 | type Builder struct { 20 | config Config 21 | } 22 | 23 | // Builder implements packersdk.Builder 24 | var _ packersdk.Builder = &Builder{} 25 | 26 | func (b *Builder) ConfigSpec() hcldec.ObjectSpec { return b.config.FlatMapstructure().HCL2Spec() } 27 | 28 | func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) { 29 | return b.config.Prepare(raws...) 30 | } 31 | 32 | func (b *Builder) Run(ctx context.Context, ui packersdk.Ui, hook packersdk.Hook) (packersdk.Artifact, error) { 33 | state := new(multistep.BasicStateBag) 34 | 35 | // prepend boot iso device to any defined additional_isos 36 | var isoArray []proxmox.ISOsConfig 37 | isoArray = append(isoArray, b.config.BootISO) 38 | isoArray = append(isoArray, b.config.ISOs...) 39 | b.config.ISOs = isoArray 40 | 41 | state.Put("iso-config", &b.config) 42 | 43 | preSteps := []multistep.Step{} 44 | postSteps := []multistep.Step{} 45 | 46 | sb := proxmox.NewSharedBuilder(BuilderID, b.config.Config, preSteps, postSteps, &isoVMCreator{}) 47 | return sb.Run(ctx, ui, hook, state) 48 | } 49 | 50 | type isoVMCreator struct{} 51 | 52 | func (*isoVMCreator) Create(vmRef *proxmoxapi.VmRef, config proxmoxapi.ConfigQemu, state multistep.StateBag) error { 53 | client := state.Get("proxmoxClient").(*proxmoxapi.Client) 54 | return config.Create(vmRef, client) 55 | } 56 | -------------------------------------------------------------------------------- /builder/proxmox/iso/config.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | //go:generate packer-sdc struct-markdown 5 | //go:generate packer-sdc mapstructure-to-hcl2 -type Config,nicConfig,diskConfig,vgaConfig,ISOsConfig 6 | 7 | package proxmoxiso 8 | 9 | import ( 10 | "errors" 11 | "fmt" 12 | "log" 13 | 14 | common "github.com/hashicorp/packer-plugin-proxmox/builder/proxmox/common" 15 | "github.com/hashicorp/packer-plugin-sdk/multistep/commonsteps" 16 | packersdk "github.com/hashicorp/packer-plugin-sdk/packer" 17 | ) 18 | 19 | type Config struct { 20 | common.Config `mapstructure:",squash"` 21 | // No longer required when deprecated boot iso options are removed 22 | commonsteps.ISOConfig `mapstructure:",squash"` 23 | // DEPRECATED. Define Boot ISO config with the `boot_iso` block instead. 24 | // Path to the ISO file to boot from, expressed as a 25 | // proxmox datastore path, for example 26 | // `local:iso/Fedora-Server-dvd-x86_64-29-1.2.iso`. 27 | // Either `iso_file` OR `iso_url` must be specifed. 28 | ISOFile string `mapstructure:"iso_file"` 29 | // DEPRECATED. Define Boot ISO config with the `boot_iso` block instead. 30 | // Proxmox storage pool onto which to upload 31 | // the ISO file. 32 | ISOStoragePool string `mapstructure:"iso_storage_pool"` 33 | // DEPRECATED. Define Boot ISO config with the `boot_iso` block instead. 34 | // Download the ISO directly from the PVE node rather than through Packer. 35 | // 36 | // Defaults to `false` 37 | ISODownloadPVE bool `mapstructure:"iso_download_pve"` 38 | // DEPRECATED. Define Boot ISO config with the `boot_iso` block instead. 39 | // If true, remove the mounted ISO from the template 40 | // after finishing. Defaults to `false`. 41 | UnmountISO bool `mapstructure:"unmount_iso"` 42 | // Boot ISO attached to the virtual machine. 43 | // 44 | // JSON Example: 45 | // 46 | // ```json 47 | // 48 | // "boot_iso": { 49 | // "type": "scsi", 50 | // "iso_file": "local:iso/debian-12.5.0-amd64-netinst.iso", 51 | // "unmount": true, 52 | // "iso_checksum": "sha512:33c08e56c83d13007e4a5511b9bf2c4926c4aa12fd5dd56d493c0653aecbab380988c5bf1671dbaea75c582827797d98c4a611f7fb2b131fbde2c677d5258ec9" 53 | // } 54 | // 55 | // ``` 56 | // HCL2 example: 57 | // 58 | // ```hcl 59 | // 60 | // boot_iso { 61 | // type = "scsi" 62 | // iso_file = "local:iso/debian-12.5.0-amd64-netinst.iso" 63 | // unmount = true 64 | // iso_checksum = "sha512:33c08e56c83d13007e4a5511b9bf2c4926c4aa12fd5dd56d493c0653aecbab380988c5bf1671dbaea75c582827797d98c4a611f7fb2b131fbde2c677d5258ec9" 65 | // } 66 | // 67 | // ``` 68 | // See [ISOs](#isos) for additional options. 69 | BootISO common.ISOsConfig `mapstructure:"boot_iso" required:"true"` 70 | } 71 | 72 | func (c *Config) Prepare(raws ...interface{}) ([]string, []string, error) { 73 | var errs *packersdk.MultiError 74 | _, warnings, merrs := c.Config.Prepare(c, raws...) 75 | if merrs != nil { 76 | errs = packersdk.MultiErrorAppend(errs, merrs) 77 | } 78 | 79 | // Convert deprecated config options 80 | if c.ISOFile != "" { 81 | warnings = append(warnings, "'iso_file' is deprecated and will be removed in a future release, define the boot iso options in a 'boot_iso' block") 82 | // Convert this field across to c.BootISO struct 83 | c.BootISO.ISOFile = c.ISOFile 84 | } 85 | if c.ISOStoragePool != "" { 86 | warnings = append(warnings, "'iso_storage_pool' is deprecated and will be removed in a future release, define the boot iso options in a 'boot_iso' block") 87 | c.BootISO.ISOStoragePool = c.ISOStoragePool 88 | } 89 | if c.ISODownloadPVE { 90 | warnings = append(warnings, "'iso_download_pve' is deprecated and will be removed in a future release, define the boot iso options in a 'boot_iso' block") 91 | c.BootISO.ISODownloadPVE = c.ISODownloadPVE 92 | } 93 | if len(c.ISOUrls) > 0 { 94 | warnings = append(warnings, "'iso_urls' is deprecated and will be removed in a future release, define the boot iso options in a 'boot_iso' block") 95 | c.BootISO.ISOUrls = c.ISOUrls 96 | } 97 | if c.RawSingleISOUrl != "" { 98 | warnings = append(warnings, "'iso_url' is deprecated and will be removed in a future release, define the boot iso options in a 'boot_iso' block") 99 | c.BootISO.RawSingleISOUrl = c.RawSingleISOUrl 100 | } 101 | if c.ISOChecksum != "" { 102 | warnings = append(warnings, "'iso_checksum' is deprecated and will be removed in a future release, define the boot iso options in a 'boot_iso' block") 103 | c.BootISO.ISOChecksum = c.ISOChecksum 104 | } 105 | if c.UnmountISO { 106 | warnings = append(warnings, "'unmount_iso' is deprecated and will be removed in a future release, define the boot iso options in a 'boot_iso' block") 107 | c.BootISO.Unmount = c.UnmountISO 108 | } 109 | if c.TargetPath != "" { 110 | warnings = append(warnings, "'iso_target_path' is deprecated and will be removed in a future release, define the boot iso options in a 'boot_iso' block") 111 | c.BootISO.TargetPath = c.TargetPath 112 | } 113 | if c.TargetExtension != "" { 114 | warnings = append(warnings, "'iso_target_extension' is deprecated and will be removed in a future release, define the boot iso options in a 'boot_iso' block") 115 | c.BootISO.TargetExtension = c.TargetExtension 116 | } 117 | 118 | // Check Boot ISO config 119 | // Either a pre-uploaded ISO should be referenced in iso_file, OR a URL 120 | // (possibly to a local file) to an ISO file that will be downloaded and 121 | // then uploaded to Proxmox. 122 | if c.BootISO.ISOFile != "" { 123 | c.BootISO.ShouldUploadISO = false 124 | } else { 125 | c.BootISO.DownloadPathKey = "downloaded_iso_path" 126 | if len(c.BootISO.CDFiles) > 0 || len(c.BootISO.CDContent) > 0 { 127 | cdErrors := c.BootISO.CDConfig.Prepare(&c.Ctx) 128 | errs = packersdk.MultiErrorAppend(errs, cdErrors...) 129 | } else { 130 | isoWarnings, isoErrors := c.BootISO.ISOConfig.Prepare(&c.Ctx) 131 | errs = packersdk.MultiErrorAppend(errs, isoErrors...) 132 | warnings = append(warnings, isoWarnings...) 133 | } 134 | c.BootISO.ShouldUploadISO = true 135 | } 136 | // validate device type, assign if unset 137 | // For backwards compatibility <= v1.8, set ide2 as default if not configured 138 | switch c.BootISO.Type { 139 | case "ide", "sata", "scsi": 140 | case "": 141 | log.Print("boot_iso device type not set, using default type 'ide' and index '2'") 142 | c.BootISO.Type = "ide" 143 | c.BootISO.Index = "2" 144 | default: 145 | errs = packersdk.MultiErrorAppend(errs, errors.New("ISOs must be of type ide, sata or scsi. VirtIO not supported by Proxmox for ISO devices")) 146 | } 147 | if len(c.BootISO.CDFiles) > 0 || len(c.BootISO.CDContent) > 0 { 148 | if c.BootISO.ISOStoragePool == "" { 149 | errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("boot_iso storage_pool not set for storage of generated ISO from cd_files or cd_content")) 150 | } 151 | } 152 | if len(c.BootISO.ISOUrls) != 0 && c.BootISO.ISOStoragePool == "" { 153 | errs = packersdk.MultiErrorAppend(errs, errors.New("when specifying iso_url in a boot_iso block, iso_storage_pool must also be specified")) 154 | } 155 | // Check only one option is present 156 | options := 0 157 | if c.BootISO.ISOFile != "" { 158 | options++ 159 | } 160 | if len(c.BootISO.ISOConfig.ISOUrls) > 0 || c.BootISO.ISOConfig.RawSingleISOUrl != "" { 161 | options++ 162 | } 163 | if len(c.BootISO.CDFiles) > 0 || len(c.BootISO.CDContent) > 0 { 164 | options++ 165 | } 166 | if options != 1 { 167 | errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("one of iso_file, iso_url, or a combination of cd_files and cd_content must be specified for boot_iso")) 168 | } 169 | if len(c.BootISO.ISOConfig.ISOUrls) == 0 && c.BootISO.ISOConfig.RawSingleISOUrl == "" && c.BootISO.ISODownloadPVE { 170 | errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("iso_download_pve can only be used together with iso_url")) 171 | } 172 | 173 | if errs != nil && len(errs.Errors) > 0 { 174 | return nil, warnings, errs 175 | } 176 | return nil, warnings, nil 177 | } 178 | -------------------------------------------------------------------------------- /builder/proxmox/iso/config_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package proxmoxiso 5 | 6 | import ( 7 | "strings" 8 | "testing" 9 | 10 | "github.com/hashicorp/packer-plugin-sdk/template" 11 | ) 12 | 13 | func TestBasicExampleFromDocsIsValid(t *testing.T) { 14 | const config = `{ 15 | "builders": [ 16 | { 17 | "type": "proxmox-iso", 18 | "proxmox_url": "https://my-proxmox.my-domain:8006/api2/json", 19 | "insecure_skip_tls_verify": true, 20 | "username": "apiuser@pve", 21 | "password": "supersecret", 22 | 23 | "node": "my-proxmox", 24 | "network_adapters": [ 25 | { 26 | "bridge": "vmbr0" 27 | } 28 | ], 29 | "disks": [ 30 | { 31 | "type": "scsi", 32 | "disk_size": "5G", 33 | "storage_pool": "local-lvm", 34 | "storage_pool_type": "lvm" 35 | } 36 | ], 37 | "boot_iso": { 38 | "type": "sata", 39 | "iso_file": "local:iso/Fedora-Server-dvd-x86_64-29-1.2.iso", 40 | "iso_storage_pool": "local-lvm", 41 | "unmount": "true" 42 | }, 43 | "http_directory":"config", 44 | "boot_wait": "10s", 45 | "boot_command": [ 46 | " ip=dhcp inst.cmdline inst.ks=http://{{.HTTPIP}}:{{.HTTPPort}}/ks.cfg" 47 | ], 48 | 49 | "ssh_username": "root", 50 | "ssh_timeout": "15m", 51 | "ssh_password": "packer", 52 | 53 | "template_name": "fedora-29", 54 | "template_description": "Fedora 29-1.2, generated on {{ isotime \"2006-01-02T15:04:05Z\" }}" 55 | } 56 | ] 57 | }` 58 | tpl, err := template.Parse(strings.NewReader(config)) 59 | if err != nil { 60 | t.Fatal(err) 61 | } 62 | 63 | b := &Builder{} 64 | _, _, err = b.Prepare(tpl.Builders["proxmox-iso"].Config) 65 | if err != nil { 66 | t.Fatal(err) 67 | } 68 | 69 | // The example config does not set a number of optional fields. Validate that: 70 | // Memory 0 is too small, using default: 512 71 | // Number of cores 0 is too small, using default: 1 72 | // Number of sockets 0 is too small, using default: 1 73 | // CPU type not set, using default 'kvm64' 74 | // OS not set, using default 'other' 75 | // NIC 0 model not set, using default 'e1000' 76 | // Disk 0 cache mode not set, using default 'none' 77 | // Agent not set, default is true 78 | // SCSI controller not set, using default 'lsi' 79 | // Firewall toggle not set, using default: 0 80 | // Disable KVM not set, using default: 0 81 | 82 | if b.config.Memory != 512 { 83 | t.Errorf("Expected Memory to be 512, got %d", b.config.Memory) 84 | } 85 | if b.config.Cores != 1 { 86 | t.Errorf("Expected Cores to be 1, got %d", b.config.Cores) 87 | } 88 | if b.config.Sockets != 1 { 89 | t.Errorf("Expected Sockets to be 1, got %d", b.config.Sockets) 90 | } 91 | if b.config.CPUType != "kvm64" { 92 | t.Errorf("Expected CPU type to be 'kvm64', got %s", b.config.CPUType) 93 | } 94 | if b.config.OS != "other" { 95 | t.Errorf("Expected OS to be 'other', got %s", b.config.OS) 96 | } 97 | if b.config.NICs[0].Model != "e1000" { 98 | t.Errorf("Expected NIC model to be 'e1000', got %s", b.config.NICs[0].Model) 99 | } 100 | if b.config.NICs[0].Firewall != false { 101 | t.Errorf("Expected NIC firewall to be false, got %t", b.config.NICs[0].Firewall) 102 | } 103 | if b.config.Disks[0].CacheMode != "none" { 104 | t.Errorf("Expected disk cache mode to be 'none', got %s", b.config.Disks[0].CacheMode) 105 | } 106 | if b.config.Agent.True() != true { 107 | t.Errorf("Expected Agent to be true, got %t", b.config.Agent.True()) 108 | } 109 | if b.config.DisableKVM != false { 110 | t.Errorf("Expected Disable KVM toggle to be false, got %t", b.config.DisableKVM) 111 | } 112 | if b.config.SCSIController != "lsi" { 113 | t.Errorf("Expected SCSI controller to be 'lsi', got %s", b.config.SCSIController) 114 | } 115 | if b.config.CloudInit != false { 116 | t.Errorf("Expected CloudInit to be false, got %t", b.config.CloudInit) 117 | } 118 | } 119 | 120 | func TestDeprecatedBootISOOptionsAreConverted(t *testing.T) { 121 | const config = `{ 122 | "builders": [ 123 | { 124 | "type": "proxmox-iso", 125 | "proxmox_url": "https://my-proxmox.my-domain:8006/api2/json", 126 | "insecure_skip_tls_verify": true, 127 | "username": "apiuser@pve", 128 | "password": "supersecret", 129 | "node": "my-proxmox", 130 | 131 | "iso_file": "local:iso/Fedora-Server-dvd-x86_64-29-1.2.iso", 132 | "unmount_iso": true, 133 | "iso_storage_pool": "local", 134 | "iso_target_path": "./test", 135 | "iso_target_extension": "img", 136 | 137 | "ssh_username": "root", 138 | "ssh_password": "packer" 139 | } 140 | ] 141 | }` 142 | tpl, err := template.Parse(strings.NewReader(config)) 143 | if err != nil { 144 | t.Fatal(err) 145 | } 146 | 147 | b := &Builder{} 148 | _, _, err = b.Prepare(tpl.Builders["proxmox-iso"].Config) 149 | if err != nil { 150 | t.Fatal(err) 151 | } 152 | 153 | // Validate that each deprecated boot ISO option is converted over to the iso struct 154 | 155 | if b.config.BootISO.ISOFile != "local:iso/Fedora-Server-dvd-x86_64-29-1.2.iso" { 156 | t.Errorf("Expected iso_file to be converted to boot_iso.iso_file: local:iso/Fedora-Server-dvd-x86_64-29-1.2.iso, got %s", b.config.BootISO.ISOFile) 157 | } 158 | if !b.config.BootISO.Unmount { 159 | t.Errorf("Expected unmount_iso to be converted to boot_iso.unmount: true, got %t", b.config.BootISO.Unmount) 160 | } 161 | if b.config.BootISO.ISOStoragePool != "local" { 162 | t.Errorf("Expected iso_storage_pool to be converted to boot_iso.iso_storage_pool: local, got %s", b.config.BootISO.ISOStoragePool) 163 | } 164 | if b.config.BootISO.TargetExtension != "img" { 165 | t.Errorf("Expected iso_target_extension to be converted to boot_iso.iso_target_extension: img, got %s", b.config.BootISO.TargetExtension) 166 | } 167 | if b.config.BootISO.TargetPath != "./test" { 168 | t.Errorf("Expected iso_target_path to be converted to boot_iso.iso_target_path: ./test, got %s", b.config.BootISO.TargetExtension) 169 | } 170 | } 171 | 172 | func TestAgentSetToFalse(t *testing.T) { 173 | cfg := mandatoryConfig(t) 174 | cfg["qemu_agent"] = false 175 | 176 | var c Config 177 | _, warn, err := c.Prepare(cfg) 178 | if err != nil { 179 | t.Fatal(err, warn) 180 | } 181 | 182 | if c.Agent.False() != true { 183 | t.Errorf("Expected Agent to be false, got %t", c.Agent.True()) 184 | } 185 | } 186 | 187 | func TestPacketQueueSupportForNetworkAdapters(t *testing.T) { 188 | drivertests := []struct { 189 | expectedToFail bool 190 | model string 191 | }{ 192 | {expectedToFail: false, model: "virtio"}, 193 | {expectedToFail: true, model: "e1000"}, 194 | {expectedToFail: true, model: "e1000-82540em"}, 195 | {expectedToFail: true, model: "e1000-82544gc"}, 196 | {expectedToFail: true, model: "e1000-82545em"}, 197 | {expectedToFail: true, model: "i82551"}, 198 | {expectedToFail: true, model: "i82557b"}, 199 | {expectedToFail: true, model: "i82559er"}, 200 | {expectedToFail: true, model: "ne2k_isa"}, 201 | {expectedToFail: true, model: "ne2k_pci"}, 202 | {expectedToFail: true, model: "pcnet"}, 203 | {expectedToFail: true, model: "rtl8139"}, 204 | {expectedToFail: true, model: "vmxnet3"}, 205 | } 206 | 207 | for _, tt := range drivertests { 208 | device := make(map[string]interface{}) 209 | device["bridge"] = "vmbr0" 210 | device["model"] = tt.model 211 | device["packet_queues"] = 2 212 | 213 | devices := make([]map[string]interface{}, 0) 214 | devices = append(devices, device) 215 | 216 | cfg := mandatoryConfig(t) 217 | cfg["network_adapters"] = devices 218 | 219 | var c Config 220 | _, _, err := c.Prepare(cfg) 221 | 222 | if tt.expectedToFail == true && err == nil { 223 | t.Error("expected config preparation to fail, but no error occured") 224 | } 225 | 226 | if tt.expectedToFail == false && err != nil { 227 | t.Errorf("expected config preparation to succeed, but %s", err.Error()) 228 | } 229 | } 230 | } 231 | 232 | func TestHardDiskControllerIOThreadSupport(t *testing.T) { 233 | drivertests := []struct { 234 | expectedToFail bool 235 | controller string 236 | disk_type string 237 | }{ 238 | // io thread is only supported by virtio-scsi-single controller 239 | // and only for virtio and scsi disks 240 | {expectedToFail: false, controller: "virtio-scsi-single", disk_type: "scsi"}, 241 | {expectedToFail: false, controller: "virtio-scsi-single", disk_type: "virtio"}, 242 | {expectedToFail: true, controller: "virtio-scsi-single", disk_type: "sata"}, 243 | {expectedToFail: true, controller: "lsi", disk_type: "scsi"}, 244 | {expectedToFail: true, controller: "lsi53c810", disk_type: "virtio"}, 245 | } 246 | 247 | for _, tt := range drivertests { 248 | nic := make(map[string]interface{}) 249 | nic["bridge"] = "vmbr0" 250 | 251 | nics := make([]map[string]interface{}, 0) 252 | nics = append(nics, nic) 253 | 254 | disk := make(map[string]interface{}) 255 | disk["type"] = tt.disk_type 256 | disk["io_thread"] = true 257 | disk["storage_pool"] = "local-lvm" 258 | disk["storage_pool_type"] = "lvm" 259 | 260 | disks := make([]map[string]interface{}, 0) 261 | disks = append(disks, disk) 262 | 263 | cfg := mandatoryConfig(t) 264 | cfg["network_adapters"] = nics 265 | cfg["disks"] = disks 266 | cfg["scsi_controller"] = tt.controller 267 | 268 | var c Config 269 | _, _, err := c.Prepare(cfg) 270 | 271 | if tt.expectedToFail == true && err == nil { 272 | t.Error("expected config preparation to fail, but no error occured") 273 | } 274 | 275 | if tt.expectedToFail == false && err != nil { 276 | t.Errorf("expected config preparation to succeed, but %s", err.Error()) 277 | } 278 | } 279 | } 280 | 281 | func mandatoryConfig(t *testing.T) map[string]interface{} { 282 | return map[string]interface{}{ 283 | "proxmox_url": "https://my-proxmox.my-domain:8006/api2/json", 284 | "username": "apiuser@pve", 285 | "password": "supersecret", 286 | "node": "my-proxmox", 287 | "ssh_username": "root", 288 | "boot_iso": map[string]interface{}{ 289 | "type": "sata", 290 | "iso_file": "local:iso/Fedora-Server-dvd-x86_64-29-1.2.iso", 291 | "iso_storage_pool": "local-lvm", 292 | "unmount": "true", 293 | }, 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /builder/proxmox/iso/testdata/test.iso: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp/packer-plugin-proxmox/8d7f07ffa3e85d730a0726a17e38ddc9687fa211/builder/proxmox/iso/testdata/test.iso -------------------------------------------------------------------------------- /docs-partials/builder/proxmox/clone/Config-not-required.mdx: -------------------------------------------------------------------------------- 1 | 2 | 3 | - `full_clone` (boolean) - Whether to run a full or shallow clone from the base clone_vm. Defaults to `true`. 4 | 5 | - `nameserver` (string) - Set nameserver IP address(es) via Cloud-Init. 6 | If not given, the same setting as on the host is used. 7 | 8 | - `searchdomain` (string) - Set the DNS searchdomain via Cloud-Init. 9 | If not given, the same setting as on the host is used. 10 | 11 | - `ipconfig` ([]cloudInitIpconfig) - Set IP address and gateway via Cloud-Init. 12 | See the [CloudInit Ip Configuration](#cloudinit-ip-configuration) documentation for fields. 13 | 14 | 15 | -------------------------------------------------------------------------------- /docs-partials/builder/proxmox/clone/Config-required.mdx: -------------------------------------------------------------------------------- 1 | 2 | 3 | - `clone_vm` (string) - The name of the VM Packer should clone and build from. 4 | Either `clone_vm` or `clone_vm_id` must be specifed. 5 | 6 | - `clone_vm_id` (int) - The ID of the VM Packer should clone and build from. 7 | Proxmox VMIDs are limited to the range 100-999999999. 8 | Either `clone_vm` or `clone_vm_id` must be specifed. 9 | 10 | 11 | -------------------------------------------------------------------------------- /docs-partials/builder/proxmox/clone/cloudInitIpconfig-not-required.mdx: -------------------------------------------------------------------------------- 1 | 2 | 3 | - `ip` (string) - Either an IPv4 address (CIDR notation) or `dhcp`. 4 | 5 | - `gateway` (string) - IPv4 gateway. 6 | 7 | - `ip6` (string) - Can be an IPv6 address (CIDR notation), `auto` (enables SLAAC), or `dhcp`. 8 | 9 | - `gateway6` (string) - IPv6 gateway. 10 | 11 | 12 | -------------------------------------------------------------------------------- /docs-partials/builder/proxmox/clone/cloudInitIpconfig.mdx: -------------------------------------------------------------------------------- 1 | 2 | 3 | If you have configured more than one network interface, make sure to match the order of 4 | `network_adapters` and `ipconfig`. 5 | 6 | Usage example (JSON): 7 | 8 | ```json 9 | [ 10 | 11 | { 12 | "ip": "192.168.1.55/24", 13 | "gateway": "192.168.1.1", 14 | "ip6": "fda8:a260:6eda:20::4da/128", 15 | "gateway6": "fda8:a260:6eda:20::1" 16 | } 17 | 18 | ] 19 | ``` 20 | 21 | 22 | -------------------------------------------------------------------------------- /docs-partials/builder/proxmox/common/Config-not-required.mdx: -------------------------------------------------------------------------------- 1 | 2 | 3 | - `boot_key_interval` (duration string | ex: "1h5m2s") - Boot Key Interval 4 | 5 | - `proxmox_url` (string) - URL to the Proxmox API, including the full path, 6 | so `https://:/api2/json` for example. 7 | Can also be set via the `PROXMOX_URL` environment variable. 8 | 9 | - `insecure_skip_tls_verify` (bool) - Skip validating the certificate. 10 | 11 | - `username` (string) - Username when authenticating to Proxmox, including 12 | the realm. For example `user@pve` to use the local Proxmox realm. When using 13 | token authentication, the username must include the token id after an exclamation 14 | mark. For example, `user@pve!tokenid`. 15 | Can also be set via the `PROXMOX_USERNAME` environment variable. 16 | 17 | - `password` (string) - Password for the user. 18 | For API tokens please use `token`. 19 | Can also be set via the `PROXMOX_PASSWORD` environment variable. 20 | Either `password` or `token` must be specifed. If both are set, 21 | `token` takes precedence. 22 | 23 | - `token` (string) - Token for authenticating API calls. 24 | This allows the API client to work with API tokens instead of user passwords. 25 | Can also be set via the `PROXMOX_TOKEN` environment variable. 26 | Either `password` or `token` must be specifed. If both are set, 27 | `token` takes precedence. 28 | 29 | - `node` (string) - Which node in the Proxmox cluster to start the virtual 30 | machine on during creation. 31 | 32 | - `pool` (string) - Name of resource pool to create virtual machine in. 33 | 34 | - `task_timeout` (duration string | ex: "1h5m2s") - `task_timeout` (duration string | ex: "10m") - The timeout for 35 | Promox API operations, e.g. clones. Defaults to 1 minute. 36 | 37 | - `vm_name` (string) - Name of the virtual machine during creation. If not 38 | given, a random uuid will be used. 39 | 40 | - `vm_id` (int) - `vm_id` (int) - The ID used to reference the virtual machine. This will 41 | also be the ID of the final template. Proxmox VMIDs are unique cluster-wide 42 | and are limited to the range 100-999999999. 43 | If not given, the next free ID on the cluster will be used. 44 | 45 | - `tags` (string) - The tags to set. This is a semicolon separated list. For example, 46 | `debian-12;template`. 47 | 48 | - `boot` (string) - Override default boot order. Format example `order=virtio0;ide2;net0`. 49 | Prior to Proxmox 6.2-15 the format was `cdn` (c:CDROM -> d:Disk -> n:Network) 50 | 51 | - `memory` (uint32) - How much memory (in megabytes) to give the virtual 52 | machine. If `ballooning_minimum` is also set, `memory` defines the maximum amount 53 | of memory the VM will be able to use. 54 | Defaults to `512`. 55 | 56 | - `ballooning_minimum` (uint32) - Setting this option enables KVM memory ballooning and 57 | defines the minimum amount of memory (in megabytes) the VM will have. 58 | Defaults to `0` (memory ballooning disabled). 59 | 60 | - `cores` (uint8) - How many CPU cores to give the virtual machine. Defaults 61 | to `1`. 62 | 63 | - `cpu_type` (string) - The CPU type to emulate. See the Proxmox API 64 | documentation for the complete list of accepted values. For best 65 | performance, set this to `host`. Defaults to `kvm64`. 66 | 67 | - `sockets` (uint8) - How many CPU sockets to give the virtual machine. 68 | Defaults to `1` 69 | 70 | - `numa` (bool) - If true, support for non-uniform memory access (NUMA) 71 | is enabled. Defaults to `false`. 72 | 73 | - `os` (string) - The operating system. Can be `wxp`, `w2k`, `w2k3`, `w2k8`, 74 | `wvista`, `win7`, `win8`, `win10`, `l24` (Linux 2.4), `l26` (Linux 2.6+), 75 | `solaris` or `other`. Defaults to `other`. 76 | 77 | - `bios` (string) - Set the machine bios. This can be set to ovmf or seabios. The default value is seabios. 78 | 79 | - `efi_config` (efiConfig) - Set the efidisk storage options. See [EFI Config](#efi-config). 80 | 81 | - `efidisk` (string) - This option is deprecated, please use `efi_config` instead. 82 | 83 | - `machine` (string) - Set the machine type. Supported values are 'pc' or 'q35'. 84 | 85 | - `rng0` (rng0Config) - Configure Random Number Generator via VirtIO. See [VirtIO RNG device](#virtio-rng-device) 86 | 87 | - `tpm_config` (tpmConfig) - Set the tpmstate storage options. See [TPM Config](#tpm-config). 88 | 89 | - `vga` (vgaConfig) - The graphics adapter to use. See [VGA Config](#vga-config). 90 | 91 | - `network_adapters` ([]NICConfig) - The network adapter to use. See [Network Adapters](#network-adapters) 92 | 93 | - `disks` ([]diskConfig) - Disks attached to the virtual machine. See [Disks](#disks) 94 | 95 | - `pci_devices` ([]pciDeviceConfig) - Allows passing through a host PCI device into the VM. See [PCI Devices](#pci-devices) 96 | 97 | - `serials` ([]string) - A list (max 4 elements) of serial ports attached to 98 | the virtual machine. It may pass through a host serial device `/dev/ttyS0` 99 | or create unix socket on the host `socket`. Each element can be `socket` 100 | or responding to pattern `/dev/.+`. Example: 101 | 102 | ```json 103 | [ 104 | "socket", 105 | "/dev/ttyS1" 106 | ] 107 | ``` 108 | 109 | - `qemu_agent` (boolean) - Enables QEMU Agent option for this VM. When enabled, 110 | then `qemu-guest-agent` must be installed on the guest. When disabled, then 111 | `ssh_host` should be used. Defaults to `true`. 112 | 113 | - `scsi_controller` (string) - The SCSI controller model to emulate. Can be `lsi`, 114 | `lsi53c810`, `virtio-scsi-pci`, `virtio-scsi-single`, `megasas`, or `pvscsi`. 115 | Defaults to `lsi`. 116 | 117 | - `onboot` (bool) - Specifies whether a VM will be started during system 118 | bootup. Defaults to `false`. 119 | 120 | - `disable_kvm` (bool) - Disables KVM hardware virtualization. Defaults to `false`. 121 | 122 | - `template_name` (string) - Name of the template. Defaults to the generated 123 | name used during creation. 124 | 125 | - `template_description` (string) - Description of the template, visible in 126 | the Proxmox interface. 127 | 128 | - `cloud_init` (bool) - If true, add an empty Cloud-Init CDROM drive after the virtual 129 | machine has been converted to a template. Defaults to `false`. 130 | 131 | - `cloud_init_storage_pool` (string) - Name of the Proxmox storage pool 132 | to store the Cloud-Init CDROM on. If not given, the storage pool of the boot device will be used. 133 | 134 | - `cloud_init_disk_type` (string) - The type of Cloud-Init disk. Can be `scsi`, `sata`, or `ide` 135 | Defaults to `ide`. 136 | 137 | - `additional_iso_files` ([]ISOsConfig) - ISO files attached to the virtual machine. 138 | See [ISOs](#isos). 139 | 140 | - `vm_interface` (string) - Name of the network interface that Packer gets 141 | the VMs IP from. Defaults to the first non loopback interface. 142 | 143 | - `qemu_additional_args` (string) - Arbitrary arguments passed to KVM. 144 | For example `-no-reboot -smbios type=0,vendor=FOO`. 145 | Note: this option is for experts only. 146 | 147 | 148 | -------------------------------------------------------------------------------- /docs-partials/builder/proxmox/common/Config.mdx: -------------------------------------------------------------------------------- 1 | 2 | 3 | There are many configuration options available for the builder. They are 4 | segmented below into two categories: required and optional parameters. Within 5 | each category, the available configuration keys are alphabetized. 6 | 7 | You may also want to take look at the general configuration references for 8 | [VirtIO RNG device](#virtio-rng-device) 9 | and [PCI Devices](#pci-devices) 10 | configuration references, which can be found further down the page. 11 | 12 | In addition to the options listed here, a 13 | [communicator](/packer/docs/templates/legacy_json_templates/communicator) can be configured for this 14 | builder. 15 | 16 | If no communicator is defined, an SSH key is generated for use, and is used 17 | in the image's Cloud-Init settings for provisioning. 18 | 19 | 20 | -------------------------------------------------------------------------------- /docs-partials/builder/proxmox/common/ISOsConfig-not-required.mdx: -------------------------------------------------------------------------------- 1 | 2 | 3 | - `device` (string) - DEPRECATED. Assign bus type with `type`. Optionally assign a bus index with `index`. 4 | Bus type and bus index that the ISO will be mounted on. Can be `ideX`, 5 | `sataX` or `scsiX`. 6 | For `ide` the bus index ranges from 0 to 3, for `sata` from 0 to 5 and for 7 | `scsi` from 0 to 30. 8 | Defaulted to `ide3` in versions up to v1.8, now defaults to dynamic ide assignment (next available ide bus index after hard disks are allocated) 9 | 10 | - `type` (string) - Bus type that the ISO will be mounted on. Can be `ide`, `sata` or `scsi`. Defaults to `ide`. 11 | 12 | - `index` (string) - Optional: Used in combination with `type` to statically assign an ISO to a bus index. 13 | 14 | - `iso_file` (string) - Path to the ISO file to boot from, expressed as a 15 | proxmox datastore path, for example 16 | `local:iso/Fedora-Server-dvd-x86_64-29-1.2.iso`. 17 | Either `iso_file` OR `iso_url` must be specifed. 18 | 19 | - `iso_storage_pool` (string) - Proxmox storage pool onto which to upload 20 | the ISO file. 21 | 22 | - `iso_download_pve` (bool) - Download the ISO directly from the PVE node rather than through Packer. 23 | 24 | Defaults to `false` 25 | 26 | - `unmount` (bool) - If true, remove the mounted ISO from the template after finishing. Defaults to `false`. 27 | 28 | - `keep_cdrom_device` (bool) - Keep CDRom device attached to template if unmounting ISO. Defaults to `false`. 29 | Has no effect if unmount is `false` 30 | 31 | 32 | -------------------------------------------------------------------------------- /docs-partials/builder/proxmox/common/ISOsConfig.mdx: -------------------------------------------------------------------------------- 1 | 2 | 3 | ISO files attached to the virtual machine. 4 | 5 | JSON Example: 6 | 7 | ```json 8 | 9 | "additional_iso_files": [ 10 | { 11 | "type": "scsi", 12 | "iso_file": "local:iso/virtio-win-0.1.185.iso", 13 | "unmount": true, 14 | "iso_checksum": "af2b3cc9fa7905dea5e58d31508d75bba717c2b0d5553962658a47aebc9cc386" 15 | } 16 | ] 17 | 18 | ``` 19 | HCL2 example: 20 | 21 | ```hcl 22 | 23 | additional_iso_files { 24 | type = "scsi" 25 | iso_file = "local:iso/virtio-win-0.1.185.iso" 26 | unmount = true 27 | iso_checksum = "af2b3cc9fa7905dea5e58d31508d75bba717c2b0d5553962658a47aebc9cc386" 28 | } 29 | 30 | ``` 31 | 32 | 33 | -------------------------------------------------------------------------------- /docs-partials/builder/proxmox/common/NICConfig-not-required.mdx: -------------------------------------------------------------------------------- 1 | 2 | 3 | - `model` (string) - Model of the virtual network adapter. Can be 4 | `rtl8139`, `ne2k_pci`, `e1000`, `pcnet`, `virtio`, `ne2k_isa`, 5 | `i82551`, `i82557b`, `i82559er`, `vmxnet3`, `e1000-82540em`, 6 | `e1000-82544gc` or `e1000-82545em`. Defaults to `e1000`. 7 | 8 | - `packet_queues` (int) - Number of packet queues to be used on the device. 9 | Values greater than 1 indicate that the multiqueue feature is activated. 10 | For best performance, set this to the number of cores available to the 11 | virtual machine. CPU load on the host and guest systems will increase as 12 | the traffic increases, so activate this option only when the VM has to 13 | handle a great number of incoming connections, such as when the VM is 14 | operating as a router, reverse proxy or a busy HTTP server. Requires 15 | `virtio` network adapter. Defaults to `0`. 16 | 17 | - `mac_address` (string) - Give the adapter a specific MAC address. If 18 | not set, defaults to a random MAC. If value is "repeatable", value of MAC 19 | address is deterministic based on VM ID and NIC ID. 20 | 21 | - `mtu` (int) - Set the maximum transmission unit for the adapter. Valid 22 | range: 0 - 65520. If set to `1`, the MTU is inherited from the bridge 23 | the adapter is attached to. Defaults to `0` (use Proxmox default). 24 | 25 | - `bridge` (string) - Required. Which Proxmox bridge to attach the 26 | adapter to. 27 | 28 | - `vlan_tag` (string) - If the adapter should tag packets. Defaults to 29 | no tagging. 30 | 31 | - `firewall` (bool) - If the interface should be protected by the firewall. 32 | Defaults to `false`. 33 | 34 | 35 | -------------------------------------------------------------------------------- /docs-partials/builder/proxmox/common/NICConfig.mdx: -------------------------------------------------------------------------------- 1 | 2 | 3 | Network adapters attached to the virtual machine. 4 | 5 | Example: 6 | 7 | ```json 8 | [ 9 | 10 | { 11 | "model": "virtio", 12 | "bridge": "vmbr0", 13 | "vlan_tag": "10", 14 | "firewall": true 15 | } 16 | 17 | ] 18 | ``` 19 | 20 | 21 | -------------------------------------------------------------------------------- /docs-partials/builder/proxmox/common/diskConfig-not-required.mdx: -------------------------------------------------------------------------------- 1 | 2 | 3 | - `type` (string) - The type of disk. Can be `scsi`, `sata`, `virtio` or 4 | `ide`. Defaults to `scsi`. 5 | 6 | - `storage_pool` (string) - Required. Name of the Proxmox storage pool 7 | to store the virtual machine disk on. A `local-lvm` pool is allocated 8 | by the installer, for example. 9 | 10 | - `storage_pool_type` (string) - This option is deprecated. 11 | 12 | - `disk_size` (string) - The size of the disk, including a unit suffix, such 13 | as `10G` to indicate 10 gigabytes. 14 | 15 | - `cache_mode` (string) - How to cache operations to the disk. Can be 16 | `none`, `writethrough`, `writeback`, `unsafe` or `directsync`. 17 | Defaults to `none`. 18 | 19 | - `format` (string) - The format of the file backing the disk. Can be 20 | `raw`, `cow`, `qcow`, `qed`, `qcow2`, `vmdk` or `cloop`. Defaults to 21 | `raw`. 22 | 23 | - `io_thread` (bool) - Create one I/O thread per storage controller, rather 24 | than a single thread for all I/O. This can increase performance when 25 | multiple disks are used. Requires `virtio-scsi-single` controller and a 26 | `scsi` or `virtio` disk. Defaults to `false`. 27 | 28 | - `asyncio` (string) - Configure Asynchronous I/O. Can be `native`, `threads`, or `io_uring`. 29 | Defaults to io_uring. 30 | 31 | - `exclude_from_backup` (bool) - Exclude disk from Proxmox backup jobs 32 | Defaults to false. 33 | 34 | - `discard` (bool) - Relay TRIM commands to the underlying storage. Defaults 35 | to false. See the 36 | [Proxmox documentation](https://pve.proxmox.com/pve-docs/pve-admin-guide.html#qm_hard_disk_discard) 37 | for for further information. 38 | 39 | - `ssd` (bool) - Drive will be presented to the guest as solid-state drive 40 | rather than a rotational disk. 41 | 42 | This cannot work with virtio disks. 43 | 44 | 45 | -------------------------------------------------------------------------------- /docs-partials/builder/proxmox/common/diskConfig.mdx: -------------------------------------------------------------------------------- 1 | 2 | 3 | Disks attached to the virtual machine. 4 | 5 | Example: 6 | 7 | ```json 8 | [ 9 | 10 | { 11 | "type": "scsi", 12 | "disk_size": "5G", 13 | "storage_pool": "local-lvm", 14 | "storage_pool_type": "lvm" 15 | } 16 | 17 | ] 18 | ``` 19 | 20 | 21 | -------------------------------------------------------------------------------- /docs-partials/builder/proxmox/common/efiConfig-not-required.mdx: -------------------------------------------------------------------------------- 1 | 2 | 3 | - `efi_storage_pool` (string) - Name of the Proxmox storage pool to store the EFI disk on. 4 | 5 | - `efi_format` (string) - The format of the file backing the disk. Can be 6 | `raw`, `cow`, `qcow`, `qed`, `qcow2`, `vmdk` or `cloop`. Defaults to 7 | `raw`. 8 | 9 | - `pre_enrolled_keys` (bool) - Whether Microsoft Standard Secure Boot keys should be pre-loaded on 10 | the EFI disk. Defaults to `false`. 11 | 12 | - `efi_type` (string) - Specifies the version of the OVMF firmware to be used. Can be `2m` or `4m`. 13 | Defaults to `4m`. 14 | 15 | 16 | -------------------------------------------------------------------------------- /docs-partials/builder/proxmox/common/efiConfig.mdx: -------------------------------------------------------------------------------- 1 | 2 | 3 | Set the efidisk storage options. 4 | This needs to be set if you use ovmf uefi boot (supersedes the `efidisk` option). 5 | 6 | Usage example (JSON): 7 | 8 | ```json 9 | 10 | { 11 | "efi_storage_pool": "local", 12 | "pre_enrolled_keys": true, 13 | "efi_format": "raw", 14 | "efi_type": "4m" 15 | } 16 | 17 | ``` 18 | 19 | 20 | -------------------------------------------------------------------------------- /docs-partials/builder/proxmox/common/pciDeviceConfig-not-required.mdx: -------------------------------------------------------------------------------- 1 | 2 | 3 | - `host` (string) - The PCI ID of a host’s PCI device or a PCI virtual function. You can us the `lspci` command to list existing PCI devices. Either this or the `mapping` key must be set. 4 | 5 | - `device_id` (string) - Override PCI device ID visible to guest. 6 | 7 | - `legacy_igd` (bool) - Pass this device in legacy IGD mode, making it the primary and exclusive graphics device in the VM. Requires `pc-i440fx` machine type and VGA set to `none`. Defaults to `false`. 8 | 9 | - `mapping` (string) - The ID of a cluster wide mapping. Either this or the `host` key must be set. 10 | 11 | - `pcie` (bool) - Present the device as a PCIe device (needs `q35` machine model). Defaults to `false`. 12 | 13 | - `mdev` (string) - The type of mediated device to use. An instance of this type will be created on startup of the VM and will be cleaned up when the VM stops. 14 | 15 | - `hide_rombar` (bool) - Specify whether or not the device’s ROM BAR will be visible in the guest’s memory map. Defaults to `false`. 16 | 17 | - `romfile` (string) - Custom PCI device rom filename (must be located in `/usr/share/kvm/`). 18 | 19 | - `sub_device_id` (string) - Override PCI subsystem device ID visible to guest. 20 | 21 | - `sub_vendor_id` (string) - Override PCI subsystem vendor ID visible to guest. 22 | 23 | - `vendor_id` (string) - Override PCI vendor ID visible to guest. 24 | 25 | - `x_vga` (bool) - Enable vfio-vga device support. Defaults to `false`. 26 | 27 | 28 | -------------------------------------------------------------------------------- /docs-partials/builder/proxmox/common/pciDeviceConfig.mdx: -------------------------------------------------------------------------------- 1 | 2 | 3 | Allows passing through a host PCI device into the VM. For example, a graphics card 4 | or a network adapter. Devices that are mapped into a guest VM are no longer available 5 | on the host. A minimal configuration only requires either the `host` or the `mapping` 6 | key to be specifed. 7 | 8 | Note: VMs with passed-through devices cannot be migrated. 9 | 10 | HCL2 example: 11 | 12 | ```hcl 13 | 14 | pci_devices { 15 | host = "0000:0d:00.1" 16 | pcie = false 17 | device_id = "1003" 18 | legacy_igd = false 19 | mdev = "some-model" 20 | hide_rombar = false 21 | romfile = "vbios.bin" 22 | sub_device_id = "" 23 | sub_vendor_id = "" 24 | vendor_id = "15B3" 25 | x_vga = false 26 | } 27 | 28 | ``` 29 | 30 | JSON example: 31 | 32 | ```json 33 | 34 | { 35 | "pci_devices": { 36 | "host" : "0000:0d:00.1", 37 | "pcie" : false, 38 | "device_id" : "1003", 39 | "legacy_igd" : false, 40 | "mdev" : "some-model", 41 | "hide_rombar" : false, 42 | "romfile" : "vbios.bin", 43 | "sub_device_id" : "", 44 | "sub_vendor_id" : "", 45 | "vendor_id" : "15B3", 46 | "x_vga" : false 47 | } 48 | } 49 | 50 | ``` 51 | 52 | 53 | -------------------------------------------------------------------------------- /docs-partials/builder/proxmox/common/rng0Config-not-required.mdx: -------------------------------------------------------------------------------- 1 | 2 | 3 | - `period` (int) - Period in milliseconds on which the the entropy-injection quota is reset. 4 | Can be a positive value. 5 | Recommended value: `1000`. 6 | 7 | 8 | -------------------------------------------------------------------------------- /docs-partials/builder/proxmox/common/rng0Config-required.mdx: -------------------------------------------------------------------------------- 1 | 2 | 3 | - `source` (string) - Device on the host to gather entropy from. 4 | `/dev/urandom` should be preferred over `/dev/random` as Proxmox PVE documentation suggests. 5 | `/dev/hwrng` can be used to pass through a hardware RNG. 6 | Can be one of `/dev/urandom`, `/dev/random`, `/dev/hwrng`. 7 | 8 | - `max_bytes` (int) - Maximum bytes of entropy allowed to get injected into the guest every `period` milliseconds. 9 | Use a lower value when using `/dev/random` since can lead to entropy starvation on the host system. 10 | `0` disables limiting and according to PVE documentation is potentially dangerous for the host. 11 | Recommended value: `1024`. 12 | 13 | 14 | -------------------------------------------------------------------------------- /docs-partials/builder/proxmox/common/rng0Config.mdx: -------------------------------------------------------------------------------- 1 | 2 | 3 | - `rng0` (object): Configure Random Number Generator via VirtIO. 4 | A virtual hardware-RNG can be used to provide entropy from the host system to a guest VM helping avoid entropy starvation which might cause the guest system slow down. 5 | The device is sourced from a host device and guest, his use can be limited: `max_bytes` bytes of data will become available on a `period` ms timer. 6 | [PVE documentation](https://pve.proxmox.com/pve-docs/pve-admin-guide.html) recommends to always use a limiter to avoid guests using too many host resources. 7 | 8 | HCL2 example: 9 | 10 | ```hcl 11 | 12 | rng0 { 13 | source = "/dev/urandom" 14 | max_bytes = 1024 15 | period = 1000 16 | } 17 | 18 | ``` 19 | 20 | JSON example: 21 | 22 | ```json 23 | 24 | { 25 | "rng0": { 26 | "source": "/dev/urandom", 27 | "max_bytes": 1024, 28 | "period": 1000 29 | } 30 | } 31 | 32 | ``` 33 | 34 | 35 | -------------------------------------------------------------------------------- /docs-partials/builder/proxmox/common/tpmConfig-not-required.mdx: -------------------------------------------------------------------------------- 1 | 2 | 3 | - `tpm_storage_pool` (string) - Name of the Proxmox storage pool to store the EFI disk on. 4 | 5 | - `tpm_version` (string) - Version of TPM spec. Can be `v1.2` or `v2.0` Defaults to `v2.0`. 6 | 7 | 8 | -------------------------------------------------------------------------------- /docs-partials/builder/proxmox/common/tpmConfig.mdx: -------------------------------------------------------------------------------- 1 | 2 | 3 | Set the tpmstate storage options. 4 | 5 | HCL2 example: 6 | 7 | ```hcl 8 | 9 | tpm_config { 10 | tpm_storage_pool = "local" 11 | tpm_version = "v1.2" 12 | } 13 | 14 | ``` 15 | Usage example (JSON): 16 | 17 | ```json 18 | 19 | "tpm_config": { 20 | "tpm_storage_pool": "local", 21 | "tpm_version": "v1.2" 22 | } 23 | 24 | ``` 25 | 26 | 27 | -------------------------------------------------------------------------------- /docs-partials/builder/proxmox/common/vgaConfig-not-required.mdx: -------------------------------------------------------------------------------- 1 | 2 | 3 | - `type` (string) - Can be `cirrus`, `none`, `qxl`,`qxl2`, `qxl3`, 4 | `qxl4`, `serial0`, `serial1`, `serial2`, `serial3`, `std`, `virtio`, `vmware`. 5 | Defaults to `std`. 6 | 7 | - `memory` (int) - How much memory to assign. 8 | 9 | 10 | -------------------------------------------------------------------------------- /docs-partials/builder/proxmox/common/vgaConfig.mdx: -------------------------------------------------------------------------------- 1 | 2 | 3 | - `vga` (object) - The graphics adapter to use. Example: 4 | 5 | ```json 6 | { 7 | "type": "vmware", 8 | "memory": 32 9 | } 10 | ``` 11 | 12 | 13 | -------------------------------------------------------------------------------- /docs-partials/builder/proxmox/iso/Config-not-required.mdx: -------------------------------------------------------------------------------- 1 | 2 | 3 | - `iso_file` (string) - DEPRECATED. Define Boot ISO config with the `boot_iso` block instead. 4 | Path to the ISO file to boot from, expressed as a 5 | proxmox datastore path, for example 6 | `local:iso/Fedora-Server-dvd-x86_64-29-1.2.iso`. 7 | Either `iso_file` OR `iso_url` must be specifed. 8 | 9 | - `iso_storage_pool` (string) - DEPRECATED. Define Boot ISO config with the `boot_iso` block instead. 10 | Proxmox storage pool onto which to upload 11 | the ISO file. 12 | 13 | - `iso_download_pve` (bool) - DEPRECATED. Define Boot ISO config with the `boot_iso` block instead. 14 | Download the ISO directly from the PVE node rather than through Packer. 15 | 16 | Defaults to `false` 17 | 18 | - `unmount_iso` (bool) - DEPRECATED. Define Boot ISO config with the `boot_iso` block instead. 19 | If true, remove the mounted ISO from the template 20 | after finishing. Defaults to `false`. 21 | 22 | 23 | -------------------------------------------------------------------------------- /docs-partials/builder/proxmox/iso/Config-required.mdx: -------------------------------------------------------------------------------- 1 | 2 | 3 | - `boot_iso` (common.ISOsConfig) - Boot ISO attached to the virtual machine. 4 | 5 | JSON Example: 6 | 7 | ```json 8 | 9 | "boot_iso": { 10 | "type": "scsi", 11 | "iso_file": "local:iso/debian-12.5.0-amd64-netinst.iso", 12 | "unmount": true, 13 | "iso_checksum": "sha512:33c08e56c83d13007e4a5511b9bf2c4926c4aa12fd5dd56d493c0653aecbab380988c5bf1671dbaea75c582827797d98c4a611f7fb2b131fbde2c677d5258ec9" 14 | } 15 | 16 | ``` 17 | HCL2 example: 18 | 19 | ```hcl 20 | 21 | boot_iso { 22 | type = "scsi" 23 | iso_file = "local:iso/debian-12.5.0-amd64-netinst.iso" 24 | unmount = true 25 | iso_checksum = "sha512:33c08e56c83d13007e4a5511b9bf2c4926c4aa12fd5dd56d493c0653aecbab380988c5bf1671dbaea75c582827797d98c4a611f7fb2b131fbde2c677d5258ec9" 26 | } 27 | 28 | ``` 29 | See [ISOs](#isos) for additional options. 30 | 31 | 32 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | The Proxmox Packer builder is able to create [Proxmox](https://www.proxmox.com/en/proxmox-ve) virtual machines and store them as new Proxmox Virtual Machine images. 2 | 3 | ### Installation 4 | 5 | To install this plugin add this code into your Packer configuration and run [packer init](/packer/docs/commands/init) 6 | 7 | ```hcl 8 | packer { 9 | required_plugins { 10 | name = { 11 | version = "~> 1" 12 | source = "github.com/hashicorp/proxmox" 13 | } 14 | } 15 | } 16 | ``` 17 | Alternatively, you can use `packer plugins install` to manage installation of this plugin. 18 | 19 | ```sh 20 | packer plugins install github.com/hashicorp/proxmox 21 | ``` 22 | 23 | ### Components 24 | 25 | Packer is able to target both ISO and existing Cloud-Init images. 26 | 27 | #### Builders 28 | 29 | - [proxmox-clone](/packer/integrations/hashicorp/proxmox/latest/components/builder/clone) - The proxmox image 30 | builder is able to create new images for use with Proxmox VE. The builder takes a cloud-init enabled virtual machine 31 | template name, runs any provisioning necessary on the image after 32 | launching it, then creates a virtual machine template. 33 | - [proxmox-iso](/packer/integrations/hashicorp/proxmox/latest/components/builder/iso) - The proxmox ISO 34 | builder is able to create new images for use with Proxmox VE. The builder 35 | takes an ISO source, runs any provisioning necessary on the image after 36 | launching it, then creates a virtual machine template. 37 | 38 | -------------------------------------------------------------------------------- /docs/builders/clone.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | description: | 3 | The proxmox image Packer builder is able to create new images for use with 4 | Proxmox VE. The builder takes a cloud-init enabled virtual machine 5 | template name, runs any provisioning necessary on the image after 6 | launching it, then creates a virtual machine template. 7 | page_title: Proxmox Clone - Builders 8 | sidebar_title: proxmox-clone 9 | nav_title: Clone 10 | --- 11 | 12 | # Proxmox Builder (from an image) 13 | 14 | Type: `proxmox-clone` 15 | Artifact BuilderId: `proxmox.clone` 16 | 17 | The `proxmox-clone` Packer builder is able to create new images for use with 18 | [Proxmox](https://www.proxmox.com/en/proxmox-ve). The builder takes a virtual 19 | machine template, runs any provisioning necessary on the image after launching it, 20 | then creates a virtual machine template. This template can then be used as to 21 | create new virtual machines within Proxmox. 22 | 23 | Disks specified in a `proxmox-clone` builder configuration will replace disks 24 | that are already present on the cloned VM template. If you want to reuse the disks 25 | of the cloned VM, don't specify disks in your configuration. 26 | 27 | The builder does _not_ manage templates. Once it creates a template, it is up 28 | to you to use it or delete it. 29 | 30 | ## Configuration Reference 31 | 32 | @include 'builder/proxmox/common/Config.mdx' 33 | 34 | ### Required: 35 | 36 | @include 'builder/proxmox/clone/Config-required.mdx' 37 | 38 | @include 'packer-plugin-sdk/multistep/commonsteps/CDConfig.mdx' 39 | 40 | @include 'packer-plugin-sdk/multistep/commonsteps/CDConfig-not-required.mdx' 41 | 42 | ### Optional: 43 | 44 | @include 'builder/proxmox/common/Config-not-required.mdx' 45 | 46 | @include 'builder/proxmox/clone/Config-not-required.mdx' 47 | 48 | ### VGA Config 49 | 50 | @include 'builder/proxmox/common/vgaConfig.mdx' 51 | 52 | #### Optional: 53 | 54 | @include 'builder/proxmox/common/vgaConfig-not-required.mdx' 55 | 56 | ### Network Adapters 57 | 58 | @include 'builder/proxmox/common/NICConfig.mdx' 59 | 60 | #### Optional: 61 | 62 | @include 'builder/proxmox/common/NICConfig-not-required.mdx' 63 | 64 | ### Disks 65 | 66 | @include 'builder/proxmox/common/diskConfig.mdx' 67 | 68 | #### Optional: 69 | 70 | @include 'builder/proxmox/common/diskConfig-not-required.mdx' 71 | 72 | ### CloudInit Ip Configuration 73 | 74 | @include 'builder/proxmox/clone/cloudInitIpconfig.mdx' 75 | 76 | @include 'builder/proxmox/clone/cloudInitIpconfig-not-required.mdx' 77 | 78 | ### ISO Files 79 | 80 | @include 'builder/proxmox/common/ISOsConfig.mdx' 81 | 82 | @include 'packer-plugin-sdk/multistep/commonsteps/ISOConfig.mdx' 83 | 84 | #### Required 85 | 86 | @include 'packer-plugin-sdk/multistep/commonsteps/ISOConfig-required.mdx' 87 | 88 | #### Optional 89 | 90 | @include 'packer-plugin-sdk/multistep/commonsteps/ISOConfig-not-required.mdx' 91 | 92 | @include 'builder/proxmox/common/ISOsConfig-not-required.mdx' 93 | 94 | @include 'packer-plugin-sdk/multistep/commonsteps/CDConfig.mdx' 95 | 96 | @include 'packer-plugin-sdk/multistep/commonsteps/CDConfig-not-required.mdx' 97 | 98 | ### EFI Config 99 | 100 | @include 'builder/proxmox/common/efiConfig.mdx' 101 | 102 | #### Optional: 103 | 104 | @include 'builder/proxmox/common/efiConfig-not-required.mdx' 105 | 106 | ### VirtIO RNG device 107 | 108 | @include 'builder/proxmox/common/rng0Config.mdx' 109 | 110 | #### Required: 111 | 112 | @include 'builder/proxmox/common/rng0Config-required.mdx' 113 | 114 | #### Optional: 115 | 116 | @include 'builder/proxmox/common/rng0Config-not-required.mdx' 117 | 118 | ### PCI devices 119 | 120 | @include 'builder/proxmox/common/pciDeviceConfig.mdx' 121 | 122 | #### Optional: 123 | 124 | @include 'builder/proxmox/common/pciDeviceConfig-not-required.mdx' 125 | 126 | ## Example: Cloud-Init enabled Debian 127 | 128 | Here is a basic example creating a Debian 10 server image. This assumes 129 | that there exists a Cloud-Init enabled image on the Proxmox server named 130 | `debian-10-4`. 131 | 132 | **HCL2** 133 | 134 | ```hcl 135 | variable "proxmox_password" { 136 | type = string 137 | default = "supersecret" 138 | } 139 | 140 | variable "proxmox_username" { 141 | type = string 142 | default = "apiuser@pve" 143 | } 144 | 145 | source "proxmox-clone" "debian" { 146 | clone_vm = "debian-10-4" 147 | cores = 1 148 | insecure_skip_tls_verify = true 149 | memory = 2048 150 | network_adapters { 151 | bridge = "vmbr0" 152 | model = "virtio" 153 | } 154 | node = "pve" 155 | os = "l26" 156 | password = "${var.proxmox_password}" 157 | pool = "api-users" 158 | proxmox_url = "https://my-proxmox.my-domain:8006/api2/json" 159 | sockets = 1 160 | ssh_username = "root" 161 | template_description = "image made from cloud-init image" 162 | template_name = "debian-scaffolding" 163 | username = "${var.proxmox_username}" 164 | } 165 | 166 | build { 167 | sources = ["source.proxmox-clone.debian"] 168 | } 169 | ``` 170 | 171 | **JSON** 172 | 173 | ```json 174 | { 175 | "variables": { 176 | "proxmox_username": "apiuser@pve", 177 | "proxmox_password": "supersecret" 178 | }, 179 | "builders": [ 180 | { 181 | "type": "proxmox-clone", 182 | "proxmox_url": "https://my-proxmox.my-domain:8006/api2/json", 183 | "username": "{{user `proxmox_username`}}", 184 | "password": "{{user `proxmox_password`}}", 185 | "ssh_username": "root", 186 | "node": "pve", 187 | "insecure_skip_tls_verify": true, 188 | "clone_vm": "debian-10-4", 189 | "template_name": "debian-scaffolding", 190 | "template_description": "image made from cloud-init image", 191 | "pool": "api-users", 192 | "os": "l26", 193 | "cores": 1, 194 | "sockets": 1, 195 | "memory": 2048, 196 | "network_adapters": [ 197 | { 198 | "model": "virtio", 199 | "bridge": "vmbr0" 200 | } 201 | ] 202 | } 203 | ] 204 | } 205 | ``` 206 | 207 | -------------------------------------------------------------------------------- /docs/builders/iso.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | description: | 3 | The proxmox ISO builder is able to create new images for use with 4 | Proxmox VE. The builder takes an ISO source, runs any provisioning 5 | necessary on the image after launching it, then creates a virtual machine 6 | template. 7 | page_title: Proxmox ISO - Builders 8 | sidebar_title: proxmox-iso 9 | nav_title: ISO 10 | --- 11 | 12 | # Proxmox Builder (from an ISO) 13 | 14 | Type: `proxmox-iso` 15 | Artifact BuilderId: `proxmox.iso` 16 | 17 | The `proxmox-iso` Packer builder is able to create new images for use with 18 | [Proxmox](https://www.proxmox.com/en/proxmox-ve). The builder takes an ISO 19 | image, runs any provisioning necessary on the image after launching it, then 20 | creates a virtual machine template. This template can then be used as to 21 | create new virtual machines within Proxmox. 22 | 23 | The builder does _not_ manage templates. Once it creates a template, it is up 24 | to you to use it or delete it. 25 | 26 | ## Configuration Reference 27 | 28 | @include 'builder/proxmox/common/Config.mdx' 29 | 30 | ### Required: 31 | 32 | @include 'builder/proxmox/iso/Config-required.mdx' 33 | 34 | ### Optional: 35 | 36 | @include 'builder/proxmox/common/Config-not-required.mdx' 37 | 38 | @include 'builder/proxmox/iso/Config-not-required.mdx' 39 | 40 | @include 'packer-plugin-sdk/multistep/commonsteps/ISOConfig-not-required.mdx' 41 | 42 | ### ISOs 43 | 44 | @include 'builder/proxmox/common/ISOsConfig.mdx' 45 | 46 | 47 | #### Required 48 | 49 | @include 'packer-plugin-sdk/multistep/commonsteps/ISOConfig-required.mdx' 50 | 51 | #### Optional 52 | 53 | @include 'packer-plugin-sdk/multistep/commonsteps/ISOConfig-not-required.mdx' 54 | 55 | @include 'builder/proxmox/common/ISOsConfig-not-required.mdx' 56 | 57 | @include 'packer-plugin-sdk/multistep/commonsteps/CDConfig.mdx' 58 | 59 | @include 'packer-plugin-sdk/multistep/commonsteps/CDConfig-not-required.mdx' 60 | 61 | ### VGA Config 62 | 63 | @include 'builder/proxmox/common/vgaConfig.mdx' 64 | 65 | #### Optional: 66 | 67 | @include 'builder/proxmox/common/vgaConfig-not-required.mdx' 68 | 69 | ### Network Adapters 70 | 71 | @include 'builder/proxmox/common/NICConfig.mdx' 72 | 73 | #### Optional: 74 | 75 | @include 'builder/proxmox/common/NICConfig-not-required.mdx' 76 | 77 | ### Disks 78 | 79 | @include 'builder/proxmox/common/diskConfig.mdx' 80 | 81 | #### Optional: 82 | 83 | @include 'builder/proxmox/common/diskConfig-not-required.mdx' 84 | 85 | ### EFI Config 86 | 87 | @include 'builder/proxmox/common/efiConfig.mdx' 88 | 89 | #### Optional: 90 | 91 | @include 'builder/proxmox/common/efiConfig-not-required.mdx' 92 | 93 | ### VirtIO RNG device 94 | 95 | @include 'builder/proxmox/common/rng0Config.mdx' 96 | 97 | #### Required: 98 | 99 | @include 'builder/proxmox/common/rng0Config-required.mdx' 100 | 101 | #### Optional: 102 | 103 | @include 'builder/proxmox/common/rng0Config-not-required.mdx' 104 | 105 | ### PCI devices 106 | 107 | @include 'builder/proxmox/common/pciDeviceConfig.mdx' 108 | 109 | #### Optional: 110 | 111 | @include 'builder/proxmox/common/pciDeviceConfig-not-required.mdx' 112 | 113 | ### Boot Command 114 | 115 | @include 'packer-plugin-sdk/bootcommand/BootConfig.mdx' 116 | 117 | #### Optional: 118 | 119 | @include 'packer-plugin-sdk/bootcommand/BootConfig-not-required.mdx' 120 | 121 | ### Http directory configuration 122 | 123 | @include 'packer-plugin-sdk/multistep/commonsteps/HTTPConfig.mdx' 124 | 125 | #### Optional: 126 | 127 | @include 'packer-plugin-sdk/multistep/commonsteps/HTTPConfig-not-required.mdx' 128 | 129 | - `http_interface` - (string) - Name of the network interface that Packer gets 130 | `HTTPIP` from. Defaults to the first non loopback interface. 131 | 132 | ## Example: Fedora with kickstart 133 | 134 | Here is a basic example creating a Fedora 29 server image with a Kickstart 135 | file served with Packer's HTTP server. Note that the iso file needs to be 136 | manually downloaded. 137 | 138 | **HCL2** 139 | 140 | ```hcl 141 | variable "password" { 142 | type = string 143 | default = "supersecret" 144 | } 145 | 146 | variable "username" { 147 | type = string 148 | default = "apiuser@pve" 149 | } 150 | 151 | source "proxmox-iso" "fedora-kickstart" { 152 | boot_command = [" ip=dhcp inst.cmdline inst.ks=http://{{ .HTTPIP }}:{{ .HTTPPort }}/ks.cfg"] 153 | boot_wait = "10s" 154 | disks { 155 | disk_size = "5G" 156 | storage_pool = "local-lvm" 157 | type = "scsi" 158 | } 159 | efi_config { 160 | efi_storage_pool = "local-lvm" 161 | efi_type = "4m" 162 | pre_enrolled_keys = true 163 | } 164 | http_directory = "config" 165 | insecure_skip_tls_verify = true 166 | iso { 167 | iso_file = "local:iso/Fedora-Server-dvd-x86_64-29-1.2.iso" 168 | } 169 | network_adapters { 170 | bridge = "vmbr0" 171 | model = "virtio" 172 | } 173 | node = "my-proxmox" 174 | password = "${var.password}" 175 | proxmox_url = "https://my-proxmox.my-domain:8006/api2/json" 176 | ssh_password = "packer" 177 | ssh_timeout = "15m" 178 | ssh_username = "root" 179 | template_description = "Fedora 29-1.2, generated on ${timestamp()}" 180 | template_name = "fedora-29" 181 | username = "${var.username}" 182 | } 183 | 184 | build { 185 | sources = ["source.proxmox-iso.fedora-kickstart"] 186 | } 187 | ``` 188 | 189 | **JSON** 190 | 191 | ```json 192 | { 193 | "variables": { 194 | "username": "apiuser@pve", 195 | "password": "supersecret" 196 | }, 197 | "builders": [ 198 | { 199 | "type": "proxmox-iso", 200 | "proxmox_url": "https://my-proxmox.my-domain:8006/api2/json", 201 | "insecure_skip_tls_verify": true, 202 | "username": "{{user `username`}}", 203 | "password": "{{user `password`}}", 204 | "node": "my-proxmox", 205 | "network_adapters": [ 206 | { 207 | "model": "virtio", 208 | "bridge": "vmbr0" 209 | } 210 | ], 211 | "disks": [ 212 | { 213 | "type": "scsi", 214 | "disk_size": "5G", 215 | "storage_pool": "local-lvm" 216 | } 217 | ], 218 | "efi_config": { 219 | "efi_storage_pool": "local-lvm", 220 | "pre_enrolled_keys": true, 221 | "efi_type": "4m" 222 | }, 223 | "iso": { 224 | "iso_file": "local:iso/Fedora-Server-dvd-x86_64-29-1.2.iso" 225 | }, 226 | "http_directory": "config", 227 | "boot_wait": "10s", 228 | "boot_command": [ 229 | " ip=dhcp inst.cmdline inst.ks=http://{{.HTTPIP}}:{{.HTTPPort}}/ks.cfg" 230 | ], 231 | "ssh_username": "root", 232 | "ssh_timeout": "15m", 233 | "ssh_password": "packer", 234 | "template_name": "fedora-29", 235 | "template_description": "Fedora 29-1.2, generated on {{ isotime \"2006-01-02T15:04:05Z\" }}" 236 | } 237 | ] 238 | } 239 | ``` 240 | 241 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/hashicorp/packer-plugin-proxmox 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.7 6 | 7 | require ( 8 | github.com/Telmate/proxmox-api-go v0.0.0-20241022204517-b149708f750b 9 | github.com/hashicorp/go-getter/v2 v2.2.2 10 | github.com/hashicorp/hcl/v2 v2.19.1 11 | github.com/hashicorp/packer-plugin-sdk v0.6.1 12 | github.com/mitchellh/mapstructure v1.5.0 13 | github.com/stretchr/testify v1.10.0 14 | github.com/zclconf/go-cty v1.13.3 15 | ) 16 | 17 | require ( 18 | cloud.google.com/go v0.110.8 // indirect 19 | cloud.google.com/go/compute v1.23.1 // indirect 20 | cloud.google.com/go/compute/metadata v0.2.3 // indirect 21 | cloud.google.com/go/iam v1.1.3 // indirect 22 | cloud.google.com/go/storage v1.35.1 // indirect 23 | github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c // indirect 24 | github.com/ChrisTrenkamp/goxpath v0.0.0-20210404020558-97928f7e12b6 // indirect 25 | github.com/agext/levenshtein v1.2.3 // indirect 26 | github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect 27 | github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect 28 | github.com/armon/go-metrics v0.4.1 // indirect 29 | github.com/aws/aws-sdk-go v1.44.122 // indirect 30 | github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect 31 | github.com/cenkalti/backoff/v3 v3.2.2 // indirect 32 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 33 | github.com/dylanmei/iso8601 v0.1.0 // indirect 34 | github.com/fatih/color v1.16.0 // indirect 35 | github.com/go-jose/go-jose/v4 v4.0.5 // indirect 36 | github.com/gofrs/flock v0.8.1 // indirect 37 | github.com/gofrs/uuid v4.0.0+incompatible // indirect 38 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 39 | github.com/golang/protobuf v1.5.3 // indirect 40 | github.com/google/s2a-go v0.1.7 // indirect 41 | github.com/google/uuid v1.4.0 // indirect 42 | github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect 43 | github.com/googleapis/gax-go/v2 v2.12.0 // indirect 44 | github.com/hashicorp/consul/api v1.25.1 // indirect 45 | github.com/hashicorp/errwrap v1.1.0 // indirect 46 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 47 | github.com/hashicorp/go-getter/gcs/v2 v2.2.2 // indirect 48 | github.com/hashicorp/go-getter/s3/v2 v2.2.2 // indirect 49 | github.com/hashicorp/go-hclog v1.6.3 // indirect 50 | github.com/hashicorp/go-immutable-radix v1.3.1 // indirect 51 | github.com/hashicorp/go-multierror v1.1.1 // indirect 52 | github.com/hashicorp/go-retryablehttp v0.7.7 // indirect 53 | github.com/hashicorp/go-rootcerts v1.0.2 // indirect 54 | github.com/hashicorp/go-safetemp v1.0.0 // indirect 55 | github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 // indirect 56 | github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect 57 | github.com/hashicorp/go-sockaddr v1.0.7 // indirect 58 | github.com/hashicorp/go-version v1.6.0 // indirect 59 | github.com/hashicorp/golang-lru v0.5.4 // indirect 60 | github.com/hashicorp/hcl v1.0.0 // indirect 61 | github.com/hashicorp/serf v0.10.1 // indirect 62 | github.com/hashicorp/vault/api v1.14.0 // indirect 63 | github.com/hashicorp/yamux v0.1.1 // indirect 64 | github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869 // indirect 65 | github.com/jmespath/go-jmespath v0.4.0 // indirect 66 | github.com/klauspost/compress v1.11.2 // indirect 67 | github.com/kr/fs v0.1.0 // indirect 68 | github.com/masterzen/simplexml v0.0.0-20190410153822-31eea3082786 // indirect 69 | github.com/masterzen/winrm v0.0.0-20210623064412-3b76017826b0 // indirect 70 | github.com/mattn/go-colorable v0.1.13 // indirect 71 | github.com/mattn/go-isatty v0.0.20 // indirect 72 | github.com/mitchellh/go-fs v0.0.0-20180402235330-b7b9ca407fff // indirect 73 | github.com/mitchellh/go-homedir v1.1.0 // indirect 74 | github.com/mitchellh/go-testing-interface v1.14.1 // indirect 75 | github.com/mitchellh/go-wordwrap v1.0.1 // indirect 76 | github.com/mitchellh/iochan v1.0.0 // indirect 77 | github.com/mitchellh/reflectwalk v1.0.0 // indirect 78 | github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect 79 | github.com/packer-community/winrmcp v0.0.0-20180921211025-c76d91c1e7db // indirect 80 | github.com/pkg/sftp v1.13.2 // indirect 81 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 82 | github.com/ryanuber/go-glob v1.0.0 // indirect 83 | github.com/ugorji/go/codec v1.2.6 // indirect 84 | github.com/ulikunitz/xz v0.5.10 // indirect 85 | github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect 86 | github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 87 | go.opencensus.io v0.24.0 // indirect 88 | golang.org/x/crypto v0.36.0 // indirect 89 | golang.org/x/exp v0.0.0-20230321023759-10a507213a29 // indirect 90 | golang.org/x/mobile v0.0.0-20210901025245-1fde1d6c3ca1 // indirect 91 | golang.org/x/net v0.37.0 // indirect 92 | golang.org/x/oauth2 v0.13.0 // indirect 93 | golang.org/x/sync v0.12.0 // indirect 94 | golang.org/x/sys v0.31.0 // indirect 95 | golang.org/x/term v0.30.0 // indirect 96 | golang.org/x/text v0.23.0 // indirect 97 | golang.org/x/time v0.11.0 // indirect 98 | golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect 99 | google.golang.org/api v0.150.0 // indirect 100 | google.golang.org/appengine v1.6.7 // indirect 101 | google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b // indirect 102 | google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b // indirect 103 | google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405 // indirect 104 | google.golang.org/grpc v1.59.0 // indirect 105 | google.golang.org/protobuf v1.33.0 // indirect 106 | gopkg.in/yaml.v2 v2.4.0 // indirect 107 | gopkg.in/yaml.v3 v3.0.1 // indirect 108 | ) 109 | 110 | replace github.com/zclconf/go-cty => github.com/nywilken/go-cty v1.13.3 // added by packer-sdc fix as noted in github.com/hashicorp/packer-plugin-sdk/issues/187 111 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | 10 | "github.com/hashicorp/packer-plugin-sdk/plugin" 11 | 12 | proxmoxclone "github.com/hashicorp/packer-plugin-proxmox/builder/proxmox/clone" 13 | proxmoxiso "github.com/hashicorp/packer-plugin-proxmox/builder/proxmox/iso" 14 | "github.com/hashicorp/packer-plugin-proxmox/version" 15 | ) 16 | 17 | func main() { 18 | pps := plugin.NewSet() 19 | // When the builder was split, the alias "proxmox" was added to Packer for the iso builder. 20 | // Registering 'plugin.DEFAULT_NAME' does the same for the external plugin. 21 | pps.RegisterBuilder(plugin.DEFAULT_NAME, new(proxmoxiso.Builder)) 22 | pps.RegisterBuilder("iso", new(proxmoxiso.Builder)) 23 | pps.RegisterBuilder("clone", new(proxmoxclone.Builder)) 24 | pps.SetVersion(version.PluginVersion) 25 | err := pps.Run() 26 | if err != nil { 27 | fmt.Fprintln(os.Stderr, err.Error()) 28 | os.Exit(1) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /version/version.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package version 5 | 6 | import "github.com/hashicorp/packer-plugin-sdk/version" 7 | 8 | var ( 9 | Version = "1.2.3" 10 | VersionPrerelease = "dev" 11 | VersionMetadata = "" 12 | PluginVersion = version.NewPluginVersion(Version, VersionPrerelease, VersionMetadata) 13 | ) 14 | --------------------------------------------------------------------------------