├── .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 │ ├── jira.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 │ │ └── qemu │ │ └── README.md ├── metadata.hcl └── scripts │ └── compile-to-webdocs.sh ├── CHANGELOG.md ├── CODEOWNERS ├── GNUmakefile ├── LICENSE ├── README.md ├── builder └── qemu │ ├── artifact.go │ ├── builder.go │ ├── comm_config.go │ ├── comm_config_test.go │ ├── config.go │ ├── config.hcl2spec.go │ ├── config_test.go │ ├── driver.go │ ├── driver_mock.go │ ├── qmp.go │ ├── ssh.go │ ├── step_configure_qmp.go │ ├── step_configure_vnc.go │ ├── step_convert_disk.go │ ├── step_convert_disk_test.go │ ├── step_copy_disk.go │ ├── step_copy_disk_test.go │ ├── step_create_disk.go │ ├── step_create_disk_test.go │ ├── step_create_vtpm.go │ ├── step_http_ip_discover.go │ ├── step_http_ip_discover_test.go │ ├── step_port_forward.go │ ├── step_prepare_efivars.go │ ├── step_prepare_output_dir.go │ ├── step_resize_disk.go │ ├── step_resize_disk_test.go │ ├── step_run.go │ ├── step_run_test.go │ ├── step_set_iso.go │ ├── step_shutdown.go │ ├── step_shutdown_test.go │ ├── step_test.go │ ├── step_type_boot_command.go │ ├── step_wait_guest_address.go │ └── testdata │ └── floppies │ ├── bar.bat │ └── foo.ps1 ├── docs-partials └── builder │ └── qemu │ ├── CommConfig-not-required.mdx │ ├── Config-not-required.mdx │ ├── QemuEFIBootConfig-not-required.mdx │ ├── QemuEFIBootConfig.mdx │ ├── QemuImgArgs-not-required.mdx │ ├── QemuSMPConfig-not-required.mdx │ └── QemuSMPConfig.mdx ├── docs ├── README.md └── builders │ └── qemu.mdx ├── example ├── README.md ├── build.pkr.hcl ├── efi_build │ ├── efi-debian.pkr.hcl │ ├── efi_data │ │ ├── OVMF_CODE_4M.ms.fd │ │ └── OVMF_VARS_4M.ms.fd │ └── http │ │ └── preseed.cfg ├── http │ └── preseed.cfg └── source.pkr.hcl ├── go.mod ├── go.sum ├── main.go └── version └── version.go /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Qemu Plugin 2 | 3 | **First:** if you're unsure or afraid of _anything_, just ask or submit the 4 | issue or pull request anyway. You won't be yelled at for giving your best 5 | effort. The worst that can happen is that you'll be politely asked to change 6 | something. We appreciate any sort of contributions, and don't want a wall of 7 | rules to get in the way of that. 8 | 9 | However, for those individuals who want a bit more guidance on the best way to 10 | contribute to the project, read on. This document will cover what we're looking 11 | for. By addressing all the points we're looking for, it raises the chances we 12 | can quickly merge or address your contributions. 13 | 14 | When contributing in any way to the Packer project (new issue, PR, etc), please 15 | be aware that our team identifies with many gender pronouns. Please remember to 16 | use nonbinary pronouns (they/them) and gender neutral language ("Hello folks") 17 | when addressing our team. For more reading on our code of conduct, please see the 18 | [HashiCorp community guidelines](https://www.hashicorp.com/community-guidelines). 19 | 20 | ## Issues 21 | 22 | ### Reporting an Issue 23 | 24 | - Make sure you test against the latest released version. It is possible we 25 | already fixed the bug you're experiencing. 26 | 27 | - Run packer with debug output with the environment variable `PACKER_LOG`. 28 | For example: `PACKER_LOG=1 packer build template.pkr.hcl`. Take the _entire_ 29 | output and create a [gist](https://gist.github.com) for linking to in your 30 | issue. Packer should strip sensitive keys from the output, but take a look 31 | through just in case. 32 | 33 | - Provide a reproducible test case. If a contributor can't reproduce an issue, 34 | then it dramatically lowers the chances it'll get fixed. And in some cases, 35 | the issue will eventually be closed. 36 | 37 | - Respond promptly to any questions made by the Packer team to your issue. Stale 38 | issues will be closed. 39 | 40 | ### Issue Lifecycle 41 | 42 | 1. The issue is reported. 43 | 44 | 2. The issue is verified and categorized by a Packer collaborator. 45 | Categorization is done via tags. For example, bugs are marked as "bugs" and 46 | simple fixes are marked as "good first issue". 47 | 48 | 3. Unless it is critical, the issue is left for a period of time (sometimes many 49 | weeks), giving outside contributors a chance to address the issue. 50 | 51 | 4. The issue is addressed in a pull request or commit. The issue will be 52 | referenced in the commit message so that the code that fixes it is clearly 53 | linked. 54 | 55 | 5. Sometimes, if you have a specialized environment or use case, the maintainers 56 | may ask for your help testing the patch. 57 | 58 | 6. The issue is closed. 59 | 60 | 61 | 62 | ## Setting up Go 63 | 64 | If you have never worked with Go before, you will have to install its 65 | runtime in order to build packer with the Qemu plugin. 66 | 67 | 1. This project always releases from the latest version of golang. 68 | [Install go](https://golang.org/doc/install#install) To properly build from 69 | source, you need to have golang >= 1.21 70 | 71 | ## Setting up Qemu plugin for dev 72 | 73 | With Go installed, you can already `go get` the Qemu plugin and `make dev` in 74 | order to compile and test it. These instructions target 75 | POSIX-like environments (macOS, Linux, Cygwin, etc.) so you may need to 76 | adjust them for Windows or other shells. 77 | 78 | 1. Download the Qemu plugin source (and its dependencies) by running 79 | `go get github.com/hashicorp/packer-plugin-qemu`. This will download the source to 80 | `$GOPATH/src/github.com/hashicorp/packer-plugin-qemu`. 81 | 82 | 2. When working on the Qemu plugin, first `cd $GOPATH/src/github.com/hashicorp/packer-plugin-qemu` 83 | so you can run `make dev` and easily access other files. `make dev` will build the packer-plugin-qemu binary and install it under `$HOME/.packer.d/plugins/`. 84 | 85 | 3. Make your changes to the Qemu plugin source. You can run `make dev` to build and install locally, and `make test` to run unit tests. 86 | Any compilation errors will be shown when the binaries are rebuilding. If you don't have `make` you can simply run `go build -o packer-plugin-qemu` from the project root, and `mv packer-plugin-qemu ~/.packer.d/plugins/packer-plugin-qemu` to install the plugin. 87 | 88 | 4. After building the Qemu plugin successfully, use the latest version of Packer to build a machine and verify your changes. In the [example folder](https://github.com/hashicorp/packer-plugin-qemu/blob/main/example) we provide a basic template. Comment out the `packer {}` block to force Packer use the development binary installed in the previous step. 89 | 90 | 5. If everything works well and the tests pass, run `go fmt ./...` on your code before 91 | submitting a pull-request. 92 | 93 | 94 | ### Opening a Pull Request 95 | 96 | Thank you for contributing! When you are ready to open a pull-request, you will 97 | need to [fork the Qemu plugin](https://github.com/hashicorp/packer-plugin-qemu#fork-destination-box), push your 98 | changes to your fork, and then open a pull-request. 99 | 100 | For example, my github username is `myuser`, so I would do the following: 101 | 102 | ``` 103 | git checkout -b f-my-feature 104 | # Develop a patch. 105 | git push https://github.com/myuser/packer-plugin-qemu f-my-feature 106 | ``` 107 | 108 | From there, open your fork in your browser to open a new pull-request. 109 | 110 | ### Pull Request Lifecycle 111 | 112 | 1. You are welcome to submit your pull request for commentary or review before 113 | it is fully completed. Please prefix the title of your pull request with 114 | "[WIP]" to indicate this. It's also a good idea to include specific questions 115 | or items you'd like feedback on. 116 | 117 | 2. Once you believe your pull request is ready to be merged, you can remove any 118 | "[WIP]" prefix from the title and a core team member will review. 119 | 120 | 3. One of Qemu plugin's core team members will look over your contribution and 121 | either merge, or provide comments letting you know if there is anything left 122 | to do. We do our best to provide feedback in a timely manner, but it may take 123 | some time for us to respond. We may also have questions that we need answered 124 | about the code, either because something doesn't make sense to us or because 125 | we want to understand your thought process. 126 | 127 | 4. If we have requested changes, you can either make those changes or, if you 128 | disagree with the suggested changes, we can have a conversation about our 129 | reasoning and agree on a path forward. This may be a multi-step process. Our 130 | view is that pull requests are a chance to collaborate, and we welcome 131 | conversations about how to do things better. It is the contributor's 132 | responsibility to address any changes requested. While reviewers are happy to 133 | give guidance, it is unsustainable for us to perform the coding work necessary 134 | to get a PR into a mergeable state. 135 | 136 | 5. Once all outstanding comments and checklist items have been addressed, your 137 | contribution will be merged! Merged PRs will be included in the next 138 | Packer release. The core team takes care of updating the 139 | [CHANGELOG.md](../CHANGELOG.md) as they merge. 140 | 141 | 6. In rare cases, we might decide that a PR should be closed without merging. 142 | We'll make sure to provide clear reasoning when this happens. 143 | 144 | ### Tips for Working on Packer 145 | 146 | #### Getting Your Pull Requests Merged Faster 147 | 148 | It is much easier to review pull requests that are: 149 | 150 | 1. Well-documented: Try to explain in the pull request comments what your 151 | change does, why you have made the change, and provide instructions for how 152 | to produce the new behavior introduced in the pull request. If you can, 153 | provide screen captures or terminal output to show what the changes look 154 | like. This helps the reviewers understand and test the change. 155 | 156 | 2. Small: Try to only make one change per pull request. If you found two bugs 157 | and want to fix them both, that's _awesome_, but it's still best to submit 158 | the fixes as separate pull requests. This makes it much easier for reviewers 159 | to keep in their heads all of the implications of individual code changes, 160 | and that means the PR takes less effort and energy to merge. In general, the 161 | smaller the pull request, the sooner reviewers will be able to make time to 162 | review it. 163 | 164 | 3. Passing Tests: Based on how much time we have, we may not review pull 165 | requests which aren't passing our tests. (Look below for advice on how to 166 | run unit tests). If you need help figuring out why tests are failing, please 167 | feel free to ask, but while we're happy to give guidance it is generally 168 | your responsibility to make sure that tests are passing. If your pull request 169 | changes an interface or invalidates an assumption that causes a bunch of 170 | tests to fail, then you need to fix those tests before we can merge your PR. 171 | 172 | If we request changes, try to make those changes in a timely manner. Otherwise, 173 | PRs can go stale and be a lot more work for all of us to merge in the future. 174 | 175 | Even with everyone making their best effort to be responsive, it can be 176 | time-consuming to get a PR merged. It can be frustrating to deal with 177 | the back-and-forth as we make sure that we understand the changes fully. Please 178 | bear with us, and please know that we appreciate the time and energy you put 179 | into the project. 180 | 181 | #### Working on forks 182 | 183 | The easiest way to work on a fork is to set it as a remote of the the Qemu plugin 184 | project. After following the steps in "Setting up Go to work on the Qemu plugin": 185 | 186 | 1. Navigate to the code: 187 | 188 | `cd $GOPATH/src/github.com/hashicorp/packer-plugin-qemu` 189 | 190 | 2. Add the remote by running: 191 | 192 | `git remote add ` 193 | 194 | For example: 195 | 196 | `git remote add myuser https://github.com/myuser/packer-plugin-qemu.git` 197 | 198 | 3. Checkout a feature branch: 199 | 200 | `git checkout -b new-feature` 201 | 202 | 4. Make changes. 203 | 5. (Optional) Push your changes to the fork: 204 | 205 | `git push -u new-feature` 206 | 207 | This way you can push to your fork to create a PR, but the code on disk still 208 | lives in the spot where the go cli tools are expecting to find it. 209 | 210 | #### Go modules & go vendor 211 | 212 | If you are submitting a change that requires new or updated dependencies, 213 | please include them in `go.mod`/`go.sum`. This 214 | helps everything get tested properly in CI. 215 | 216 | Note that you will need to use [go 217 | mod](https://github.com/golang/go/wiki/Modules) to do this. This step is 218 | recommended but not required. 219 | 220 | Use `go get ` to add dependencies to the project. See [go mod quick 221 | start](https://github.com/golang/go/wiki/Modules#quick-start) for examples. 222 | 223 | Please only apply the minimal vendor changes to get your PR to work. The Qemu plugin 224 | does not attempt to track the latest version for each dependency. 225 | 226 | #### HCL2 Spec code generation 227 | 228 | Packer relies on `go generate` to generate HCL2's bridging code. First you should install the command with `go install github.com/hashicorp/packer-plugin-sdk/cmd/packer-sdc@latest`, then you generate with 229 | `go generate ./...`. This should update/create `*.hcl2spec.go` file(s). 230 | 231 | #### Running Unit Tests 232 | 233 | To run unit tests, simply call 234 | 235 | ``` 236 | make test 237 | ``` 238 | 239 | from the plugin's project root. 240 | 241 | #### Running Builder Acceptance Tests 242 | 243 | If the Qemu Plugin has [acceptance tests](https://en.wikipedia.org/wiki/Acceptance_testing), these probably have some requirements such as environment variables to be set for API tokens and keys. Each test should error and tell you what are missing, so those are not documented here. 244 | 245 | If you're working on a feature and want to verify it is functioning (and also hasn't broken anything else), we recommend creating or running the acceptance tests. 246 | 247 | **Warning:** The acceptance tests create/destroy/modify _real resources_, which 248 | may incur costs for real money. In the presence of a bug, it is possible that 249 | resources may be left behind, which can cost money even though you were not 250 | using them. We recommend running tests in an account used only for that purpose 251 | so it is easy to see if there are any dangling resources, and so production 252 | resources are not accidentally destroyed or overwritten during testing. 253 | 254 | To run the acceptance tests, invoke `make testacc`: 255 | 256 | ``` 257 | make testacc 258 | ... 259 | ``` 260 | 261 | The `TEST` variable lets you narrow the scope of the acceptance tests to a 262 | specific package / folder. 263 | 264 | #### Debugging Plugins 265 | 266 | Each packer plugin runs in a separate process and communicates via RPC over a 267 | socket therefore using a debugger will not work (be complicated at least). 268 | 269 | The Packer and plugins code provides more detailed logs when ran with PACKER_LOG 270 | turned on. If that doesn't work, adding some extra debug print outs when you have 271 | homed in on the problem is usually enough. 272 | -------------------------------------------------------------------------------- /.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-qemu/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_qemu_${{ inputs.GOOS }}_${{ inputs.GOARCH }} ." 15 | shell: bash 16 | - run: zip ./pkg/packer_plugin_qemu_${{ inputs.GOOS }}_${{ inputs.GOARCH }}.zip ./pkg/packer_plugin_qemu_${{ inputs.GOOS }}_${{ inputs.GOARCH }} 17 | shell: bash 18 | - run: rm ./pkg/packer_plugin_qemu_${{ inputs.GOOS }}_${{ inputs.GOARCH }} 19 | shell: bash 20 | - uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 21 | with: 22 | name: "packer_plugin_qemu_${{ inputs.GOOS }}_${{ inputs.GOARCH }}.zip" 23 | path: "pkg/packer_plugin_qemu_${{ 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-qemu/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-qemu 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-qemu 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-qemu 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-qemu 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-qemu 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-qemu 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-qemu 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 | 148 | build_linux_ppc64le: 149 | defaults: 150 | run: 151 | working-directory: ~go/src/github.com/hashicorp/packer-plugin-qemu 152 | runs-on: ubuntu-latest 153 | container: 154 | image: docker.mirror.hashicorp.services/cimg/go:1.21 155 | steps: 156 | - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 157 | - uses: "./.github/actions/build-and-persist-plugin-binary" 158 | with: 159 | GOOS: linux 160 | GOARCH: ppc64le 161 | -------------------------------------------------------------------------------- /.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/jira.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | name: JIRA Sync 5 | on: 6 | issues: 7 | types: [labeled] 8 | permissions: 9 | contents: read 10 | jobs: 11 | sync: 12 | name: Sync to JIRA 13 | permissions: 14 | issues: write # for actions-ecosytem/action-create-comment 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Login 18 | uses: atlassian/gajira-login@45fd029b9f1d6d8926c6f04175aa80c0e42c9026 # v3.0.1 19 | env: 20 | JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }} 21 | JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }} 22 | JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} 23 | - name: Search 24 | if: github.event.action == 'labeled' 25 | id: search 26 | uses: tomhjp/gh-action-jira-search@04700b457f317c3e341ce90da5a3ff4ce058f2fa # v0.2.2 27 | with: 28 | # cf[10089] is Issue Link (use JIRA API to retrieve) 29 | jql: 'project = "HPR" AND cf[10089] = "${{ github.event.issue.html_url }}"' 30 | - name: Set type 31 | id: set-ticket-type 32 | run: | 33 | # Questions are not tracked in JIRA at this time. 34 | if [[ "${{ contains(github.event.issue.labels.*.name, 'question') }}" == "true" ]]; then 35 | echo "type=Invalid" >> "$GITHUB_OUTPUT" 36 | else 37 | # Properly labeled GH issues are assigned the standard "GH Issue" type upon creation. 38 | echo "type=GH Issue" >> "$GITHUB_OUTPUT" 39 | fi 40 | - name: Set labels 41 | id: set-ticket-labels 42 | run: | 43 | if [[ "${{ contains(github.event.issue.labels.*.name, 'bug') }}" == "true" ]]; then 44 | echo "labels=[\"bug\"]" >> "$GITHUB_OUTPUT" 45 | elif [[ "${{ contains(github.event.issue.labels.*.name, 'enhancement') }}" == "true" ]]; then 46 | echo "labels=[\"enhancement\"]" >> "$GITHUB_OUTPUT" 47 | else 48 | echo "labels=[]" >> "$GITHUB_OUTPUT" 49 | fi 50 | - name: Validate ticket 51 | if: steps.set-ticket-type.outputs.type == 'Invalid' 52 | run: | 53 | echo "Questions are not being synced to JIRA at this time." 54 | echo "If the issue is a bug or an enhancement please remove the question label and reapply the 'sync to jira' label." 55 | - name: Create ticket 56 | id: create-ticket 57 | if: steps.search.outputs.issue == '' && github.event.label.name == 'sync to jira' && steps.set-ticket-type.outputs.type != 'Invalid' 58 | uses: atlassian/gajira-create@59e177c4f6451399df5b4911c2211104f171e669 # v3.0.1 59 | with: 60 | project: HPR 61 | issuetype: "${{ steps.set-ticket-type.outputs.type }}" 62 | summary: "${{ github.event.repository.name }}: ${{ github.event.issue.title }}" 63 | description: "${{ github.event.issue.body }}\n\n_Created from GitHub by ${{ github.actor }}._" 64 | # The field customfield_10089 refers to the Issue Link field in JIRA. 65 | fields: '{ "customfield_10089": "${{ github.event.issue.html_url }}", 66 | "components": [{ "name": "Core" }], 67 | "labels": ${{ steps.set-ticket-labels.outputs.labels }} }' 68 | - name: Add tracking comment 69 | if: steps.create-ticket.outputs.issue != '' && steps.set-ticket-type.outputs.type != 'Invalid' 70 | uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6.4.1 71 | with: 72 | script: | 73 | github.rest.issues.createComment({ 74 | issue_number: context.issue.number, 75 | owner: context.repo.owner, 76 | repo: context.repo.repo, 77 | body: ` 78 | This issue has been synced to JIRA for planning. 79 | JIRA ID: [${{ steps.create-ticket.outputs.issue }}](https://hashicorp.atlassian.net/browse/${{steps.create-ticket.outputs.issue}})` 80 | }) 81 | -------------------------------------------------------------------------------- /.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/qemu" 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/qemu" 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-scaffolding 4 | example/output-ubuntu1804 5 | crash.log 6 | -------------------------------------------------------------------------------- /.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 | - ppc64le 46 | ignore: 47 | - goos: linux 48 | goarch: amd64 49 | binary: '{{ .ProjectName }}_v{{ .Version }}_{{ .Env.API_VERSION }}_{{ .Os }}_{{ .Arch }}' 50 | - 51 | id: darwin-builds 52 | mod_timestamp: '{{ .CommitTimestamp }}' 53 | flags: 54 | - -trimpath #removes all file system paths from the compiled executable 55 | ldflags: 56 | - '-s -w -X {{ .ModulePath }}/version.Version={{.Version}} -X {{ .ModulePath }}/version.VersionPrerelease= ' 57 | goos: 58 | - darwin 59 | goarch: 60 | - amd64 61 | - arm64 62 | binary: '{{ .ProjectName }}_v{{ .Version }}_{{ .Env.API_VERSION }}_{{ .Os }}_{{ .Arch }}' 63 | - 64 | id: other-builds 65 | mod_timestamp: '{{ .CommitTimestamp }}' 66 | flags: 67 | - -trimpath #removes all file system paths from the compiled executable 68 | ldflags: 69 | - '-s -w -X {{ .ModulePath }}/version.Version={{.Version}} -X {{ .ModulePath }}/version.VersionPrerelease= ' 70 | goos: 71 | - netbsd 72 | - openbsd 73 | - freebsd 74 | - windows 75 | - illumos 76 | - solaris 77 | goarch: 78 | - amd64 79 | - '386' 80 | - arm 81 | ignore: 82 | - goos: windows 83 | goarch: arm 84 | - goos: illumos 85 | goarch: arm 86 | - goos: illumos 87 | goarch: '386' 88 | - goos: solaris 89 | goarch: arm 90 | - goos: solaris 91 | goarch: '386' 92 | binary: '{{ .ProjectName }}_v{{ .Version }}_{{ .Env.API_VERSION }}_{{ .Os }}_{{ .Arch }}' 93 | archives: 94 | - format: zip 95 | files: 96 | - "LICENSE.txt" 97 | 98 | name_template: '{{ .ProjectName }}_v{{ .Version }}_{{ .Env.API_VERSION }}_{{ .Os }}_{{ .Arch }}' 99 | checksum: 100 | name_template: '{{ .ProjectName }}_v{{ .Version }}_SHA256SUMS' 101 | algorithm: sha256 102 | signs: 103 | - cmd: signore 104 | args: ["sign", "--dearmor", "--file", "${artifact}", "--out", "${signature}"] 105 | artifacts: checksum 106 | signature: ${artifact}.sig 107 | 108 | changelog: 109 | use: github-native 110 | -------------------------------------------------------------------------------- /.web-docs/README.md: -------------------------------------------------------------------------------- 1 | 2 | The Qemu Packer Plugin comes with a single builder able to create KVM virtual machine images. 3 | 4 | 5 | ### Installation 6 | 7 | To install this plugin add this code into your Packer configuration and run [packer init](/packer/docs/commands/init) 8 | 9 | ```hcl 10 | packer { 11 | required_plugins { 12 | qemu = { 13 | version = "~> 1" 14 | source = "github.com/hashicorp/qemu" 15 | } 16 | } 17 | } 18 | ``` 19 | Alternatively, you can use `packer plugins install` to manage installation of this plugin. 20 | 21 | ```sh 22 | packer plugins install github.com/hashicorp/qemu 23 | ``` 24 | 25 | ### Components 26 | 27 | #### Builders 28 | 29 | - [qemu](/packer/integrations/hashicorp/qemu/latest/components/builder/qemu) - The QEMU builder is able to create [KVM](http://www.linux-kvm.org) virtual machine images. 30 | 31 | -------------------------------------------------------------------------------- /.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 = "QEMU" 8 | description = "The Qemu Packer Plugin comes with a single builder able to create KVM virtual machine images." 9 | identifier = "packer/hashicorp/qemu" 10 | component { 11 | type = "builder" 12 | name = "QEMU" 13 | slug = "qemu" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.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 | # Latest Release 2 | 3 | Please refer to [releases](https://github.com/hashicorp/packer-plugin-qemu/releases) for the latest CHANGELOG information. 4 | 5 | --- 6 | ## 1.0.1 (September 28, 2021) 7 | 8 | ### IMPROVEMENTS 9 | * Add support for cd_content configuration aregument. [GH-34] 10 | * Update packer-plugin-sdk to version 0.2.5. [GH-44] 11 | 12 | ### BUG FIXES 13 | * Fix SCSI index conflicts when both cdrom and disk using virtio-scsi 14 | interface. [GH-40] 15 | 16 | ## 1.0.0 (June 14, 2021) 17 | 18 | * Update packer-plugin-sdk to version 0.2.3. [GH-29] 19 | 20 | ## 0.0.1 (April 19, 2021) 21 | 22 | * QEMU 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). 23 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @hashicorp/packer 2 | -------------------------------------------------------------------------------- /GNUmakefile: -------------------------------------------------------------------------------- 1 | NAME=qemu 2 | BINARY=packer-plugin-${NAME} 3 | PLUGIN_FQN="$(shell grep -E '^module' c.HostPortMax { 70 | errs = append(errs, 71 | errors.New("host_port_min must be less than host_port_max")) 72 | } 73 | 74 | if c.HostPortMin < 0 { 75 | errs = append(errs, errors.New("host_port_min must be positive")) 76 | } 77 | 78 | return 79 | } 80 | -------------------------------------------------------------------------------- /builder/qemu/comm_config_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package qemu 5 | 6 | import ( 7 | "io/ioutil" 8 | "os" 9 | "testing" 10 | 11 | "github.com/hashicorp/packer-plugin-sdk/communicator" 12 | "github.com/hashicorp/packer-plugin-sdk/template/interpolate" 13 | ) 14 | 15 | func testCommConfig() *CommConfig { 16 | return &CommConfig{ 17 | Comm: communicator.Config{ 18 | SSH: communicator.SSH{ 19 | SSHUsername: "foo", 20 | }, 21 | }, 22 | } 23 | } 24 | 25 | func TestCommConfigPrepare(t *testing.T) { 26 | c := testCommConfig() 27 | warns, errs := c.Prepare(interpolate.NewContext()) 28 | if len(errs) > 0 { 29 | t.Fatalf("err: %#v", errs) 30 | } 31 | if len(warns) != 0 { 32 | t.Fatal("should not have any warnings") 33 | } 34 | 35 | if c.HostPortMin != 2222 { 36 | t.Errorf("bad min communicator host port: %d", c.HostPortMin) 37 | } 38 | 39 | if c.HostPortMax != 4444 { 40 | t.Errorf("bad max communicator host port: %d", c.HostPortMax) 41 | } 42 | 43 | if c.Comm.SSHPort != 22 { 44 | t.Errorf("bad communicator port: %d", c.Comm.SSHPort) 45 | } 46 | } 47 | 48 | func TestCommConfigPrepare_SSHHostPort(t *testing.T) { 49 | var c *CommConfig 50 | var errs []error 51 | var warns []string 52 | 53 | // Bad 54 | c = testCommConfig() 55 | c.HostPortMin = 1000 56 | c.HostPortMax = 500 57 | warns, errs = c.Prepare(interpolate.NewContext()) 58 | if len(errs) == 0 { 59 | t.Fatalf("bad: %#v", errs) 60 | } 61 | if len(warns) != 0 { 62 | t.Fatal("should not have any warnings") 63 | } 64 | 65 | // Good 66 | c = testCommConfig() 67 | c.HostPortMin = 50 68 | c.HostPortMax = 500 69 | warns, errs = c.Prepare(interpolate.NewContext()) 70 | if len(errs) > 0 { 71 | t.Fatalf("should not have error: %s", errs) 72 | } 73 | if len(warns) != 0 { 74 | t.Fatal("should not have any warnings") 75 | } 76 | 77 | tc := []struct { 78 | name string 79 | host, expectedHost string 80 | skipNat bool 81 | }{ 82 | { 83 | name: "skip_nat_mapping false should not change host", 84 | host: "192.168.1.1", 85 | expectedHost: "192.168.1.1", 86 | }, 87 | { 88 | name: "skip_nat_mapping true should not change host", 89 | host: "192.168.1.1", 90 | expectedHost: "192.168.1.1", 91 | skipNat: true, 92 | }, 93 | { 94 | name: "skip_nat_mapping true with no set host", 95 | expectedHost: "127.0.0.1", 96 | skipNat: true, 97 | }, 98 | } 99 | 100 | for _, tt := range tc { 101 | tt := tt 102 | t.Run(tt.name, func(t *testing.T) { 103 | c := &CommConfig{ 104 | SkipNatMapping: tt.skipNat, 105 | Comm: communicator.Config{ 106 | SSH: communicator.SSH{ 107 | SSHUsername: "tester", 108 | SSHHost: tt.host, 109 | }, 110 | }, 111 | } 112 | warns, errs := c.Prepare(interpolate.NewContext()) 113 | if len(errs) > 0 { 114 | t.Fatalf("should not have error: %s", errs) 115 | } 116 | if len(warns) != 0 { 117 | t.Fatal("should not have any warnings") 118 | } 119 | if c.Comm.SSHHost != tt.expectedHost { 120 | t.Errorf("unexpected SSHHost: wanted: %s, got: %s", tt.expectedHost, c.Comm.SSHHost) 121 | } 122 | }) 123 | } 124 | } 125 | 126 | func TestCommConfigPrepare_SSHPrivateKey(t *testing.T) { 127 | var c *CommConfig 128 | var errs []error 129 | var warns []string 130 | 131 | c = testCommConfig() 132 | c.Comm.SSHPrivateKeyFile = "" 133 | warns, errs = c.Prepare(interpolate.NewContext()) 134 | if len(errs) > 0 { 135 | t.Fatalf("should not have error: %#v", errs) 136 | } 137 | if len(warns) != 0 { 138 | t.Fatal("should not have any warnings") 139 | } 140 | 141 | c = testCommConfig() 142 | c.Comm.SSHPrivateKeyFile = "/i/dont/exist" 143 | warns, errs = c.Prepare(interpolate.NewContext()) 144 | if len(errs) == 0 { 145 | t.Fatal("should have error") 146 | } 147 | if len(warns) != 0 { 148 | t.Fatal("should not have any warnings") 149 | } 150 | 151 | // Test bad contents 152 | tf, err := ioutil.TempFile("", "packer") 153 | if err != nil { 154 | t.Fatalf("err: %s", err) 155 | } 156 | defer os.Remove(tf.Name()) 157 | defer tf.Close() 158 | 159 | if _, err := tf.Write([]byte("HELLO!")); err != nil { 160 | t.Fatalf("err: %s", err) 161 | } 162 | 163 | c = testCommConfig() 164 | c.Comm.SSHPrivateKeyFile = tf.Name() 165 | warns, errs = c.Prepare(interpolate.NewContext()) 166 | if len(errs) == 0 { 167 | t.Fatal("should have error") 168 | } 169 | if len(warns) != 0 { 170 | t.Fatal("should not have any warnings") 171 | } 172 | 173 | // Test good contents 174 | _, err = tf.Seek(0, 0) 175 | if err != nil { 176 | t.Fatalf("err: %s", err) 177 | } 178 | err = tf.Truncate(0) 179 | if err != nil { 180 | t.Fatalf("err: %s", err) 181 | } 182 | _, err = tf.Write([]byte(testPem)) 183 | if err != nil { 184 | t.Fatalf("err: %s", err) 185 | } 186 | 187 | c = testCommConfig() 188 | c.Comm.SSHPrivateKeyFile = tf.Name() 189 | warns, errs = c.Prepare(interpolate.NewContext()) 190 | if len(errs) > 0 { 191 | t.Fatalf("should not have error: %#v", errs) 192 | } 193 | if len(warns) != 0 { 194 | t.Fatal("should not have any warnings") 195 | } 196 | } 197 | 198 | func TestCommConfigPrepare_WinHostPort(t *testing.T) { 199 | tc := []struct { 200 | name string 201 | host, expectedHost string 202 | skipNat bool 203 | }{ 204 | { 205 | name: "skip_nat_mapping false should not change host", 206 | host: "192.168.1.1", 207 | expectedHost: "192.168.1.1", 208 | }, 209 | { 210 | name: "skip_nat_mapping true should not change host", 211 | host: "192.168.1.1", 212 | expectedHost: "192.168.1.1", 213 | skipNat: true, 214 | }, 215 | { 216 | name: "skip_nat_mapping true with no set host", 217 | expectedHost: "127.0.0.1", 218 | skipNat: true, 219 | }, 220 | } 221 | 222 | for _, tt := range tc { 223 | tt := tt 224 | t.Run(tt.name, func(t *testing.T) { 225 | c := &CommConfig{ 226 | SkipNatMapping: tt.skipNat, 227 | Comm: communicator.Config{ 228 | Type: "winrm", 229 | WinRM: communicator.WinRM{ 230 | WinRMUser: "tester", 231 | WinRMHost: tt.host, 232 | }, 233 | }, 234 | } 235 | warns, errs := c.Prepare(interpolate.NewContext()) 236 | if len(errs) > 0 { 237 | t.Fatalf("should not have error: %s", errs) 238 | } 239 | if len(warns) != 0 { 240 | t.Fatal("should not have any warnings") 241 | } 242 | if c.Comm.WinRMHost != tt.expectedHost { 243 | t.Errorf("unexpected WinRMHost: wanted: %s, got: %s", tt.expectedHost, c.Comm.WinRMHost) 244 | } 245 | }) 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /builder/qemu/driver.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package qemu 5 | 6 | import ( 7 | "bufio" 8 | "bytes" 9 | "fmt" 10 | "io" 11 | "log" 12 | "os" 13 | "os/exec" 14 | "regexp" 15 | "strings" 16 | "sync" 17 | "syscall" 18 | "time" 19 | "unicode" 20 | 21 | "github.com/hashicorp/packer-plugin-sdk/multistep" 22 | ) 23 | 24 | type DriverCancelCallback func(state multistep.StateBag) bool 25 | 26 | // A driver is able to talk to qemu-system-x86_64 and perform certain 27 | // operations with it. 28 | type Driver interface { 29 | // Copy bypasses qemu-img convert and directly copies an image 30 | // that doesn't need converting. 31 | Copy(string, string) error 32 | 33 | // Stop stops a running machine, forcefully. 34 | Stop() error 35 | 36 | // Qemu executes the given command via qemu-system-x86_64 37 | Qemu(qemuArgs ...string) error 38 | 39 | // wait on shutdown of the VM with option to cancel 40 | WaitForShutdown(<-chan struct{}) bool 41 | 42 | // Qemu executes the given command via qemu-img 43 | QemuImg(...string) error 44 | 45 | // Verify checks to make sure that this driver should function 46 | // properly. If there is any indication the driver can't function, 47 | // this will return an error. 48 | Verify() error 49 | 50 | // Version reads the version of Qemu that is installed. 51 | Version() (string, error) 52 | } 53 | 54 | type QemuDriver struct { 55 | QemuPath string 56 | QemuImgPath string 57 | 58 | vmCmd *exec.Cmd 59 | vmEndCh <-chan int 60 | lock sync.Mutex 61 | } 62 | 63 | func (d *QemuDriver) Stop() error { 64 | d.lock.Lock() 65 | defer d.lock.Unlock() 66 | 67 | if d.vmCmd != nil { 68 | if err := d.vmCmd.Process.Kill(); err != nil { 69 | return err 70 | } 71 | } 72 | 73 | return nil 74 | } 75 | 76 | func (d *QemuDriver) Copy(sourceName, targetName string) error { 77 | source, err := os.Open(sourceName) 78 | if err != nil { 79 | err = fmt.Errorf("Error opening iso for copy: %s", err) 80 | return err 81 | } 82 | defer source.Close() 83 | 84 | // Create will truncate an existing file 85 | target, err := os.Create(targetName) 86 | if err != nil { 87 | err = fmt.Errorf("Error creating hard drive in output dir: %s", err) 88 | return err 89 | } 90 | defer target.Close() 91 | 92 | log.Printf("Copying %s to %s", source.Name(), target.Name()) 93 | bytes, err := io.Copy(target, source) 94 | if err != nil { 95 | err = fmt.Errorf("Error copying iso to output dir: %s", err) 96 | return err 97 | } 98 | log.Printf("Copied %d bytes", bytes) 99 | 100 | return nil 101 | } 102 | 103 | func (d *QemuDriver) Qemu(qemuArgs ...string) error { 104 | d.lock.Lock() 105 | defer d.lock.Unlock() 106 | 107 | if d.vmCmd != nil { 108 | panic("Existing VM state found") 109 | } 110 | 111 | stdout_r, stdout_w := io.Pipe() 112 | stderr_r, stderr_w := io.Pipe() 113 | 114 | log.Printf("Executing %s: %#v", d.QemuPath, qemuArgs) 115 | cmd := exec.Command(d.QemuPath, qemuArgs...) 116 | cmd.Stdout = stdout_w 117 | cmd.Stderr = stderr_w 118 | 119 | err := cmd.Start() 120 | if err != nil { 121 | err = fmt.Errorf("Error starting VM: %s", err) 122 | return err 123 | } 124 | 125 | go logReader("Qemu stdout", stdout_r) 126 | go logReader("Qemu stderr", stderr_r) 127 | 128 | log.Printf("Started Qemu. Pid: %d", cmd.Process.Pid) 129 | 130 | // Wait for Qemu to complete in the background, and mark when its done 131 | endCh := make(chan int, 1) 132 | go func() { 133 | defer stderr_w.Close() 134 | defer stdout_w.Close() 135 | 136 | var exitCode int = 0 137 | if err := cmd.Wait(); err != nil { 138 | if exiterr, ok := err.(*exec.ExitError); ok { 139 | // The program has exited with an exit code != 0 140 | if status, ok := exiterr.Sys().(syscall.WaitStatus); ok { 141 | exitCode = status.ExitStatus() 142 | } else { 143 | exitCode = 254 144 | } 145 | } 146 | } 147 | 148 | endCh <- exitCode 149 | 150 | d.lock.Lock() 151 | defer d.lock.Unlock() 152 | d.vmCmd = nil 153 | d.vmEndCh = nil 154 | }() 155 | 156 | // Wait at least a couple seconds for an early fail from Qemu so 157 | // we can report that. 158 | select { 159 | case exit := <-endCh: 160 | if exit != 0 { 161 | return fmt.Errorf("Qemu failed to start. Please run with PACKER_LOG=1 to get more info.") 162 | } 163 | case <-time.After(2 * time.Second): 164 | } 165 | 166 | // Setup our state so we know we are running 167 | d.vmCmd = cmd 168 | d.vmEndCh = endCh 169 | 170 | return nil 171 | } 172 | 173 | func (d *QemuDriver) WaitForShutdown(cancelCh <-chan struct{}) bool { 174 | d.lock.Lock() 175 | endCh := d.vmEndCh 176 | d.lock.Unlock() 177 | 178 | if endCh == nil { 179 | return true 180 | } 181 | 182 | select { 183 | case <-endCh: 184 | return true 185 | case <-cancelCh: 186 | return false 187 | } 188 | } 189 | 190 | func (d *QemuDriver) QemuImg(args ...string) error { 191 | var stdout, stderr bytes.Buffer 192 | 193 | log.Printf("Executing qemu-img: %#v", args) 194 | cmd := exec.Command(d.QemuImgPath, args...) 195 | cmd.Stdout = &stdout 196 | cmd.Stderr = &stderr 197 | err := cmd.Run() 198 | 199 | stdoutString := strings.TrimSpace(stdout.String()) 200 | stderrString := strings.TrimSpace(stderr.String()) 201 | 202 | if _, ok := err.(*exec.ExitError); ok { 203 | err = fmt.Errorf("QemuImg error: %s", stderrString) 204 | } 205 | 206 | log.Printf("stdout: %s", stdoutString) 207 | log.Printf("stderr: %s", stderrString) 208 | 209 | return err 210 | } 211 | 212 | func (d *QemuDriver) Verify() error { 213 | return nil 214 | } 215 | 216 | func (d *QemuDriver) Version() (string, error) { 217 | var stdout bytes.Buffer 218 | 219 | cmd := exec.Command(d.QemuPath, "-version") 220 | cmd.Stdout = &stdout 221 | if err := cmd.Run(); err != nil { 222 | return "", err 223 | } 224 | 225 | versionOutput := strings.TrimSpace(stdout.String()) 226 | log.Printf("Qemu --version output: %s", versionOutput) 227 | versionRe := regexp.MustCompile(`[\.[0-9]+]*`) 228 | matches := versionRe.FindStringSubmatch(versionOutput) 229 | if len(matches) == 0 { 230 | return "", fmt.Errorf("No version found: %s", versionOutput) 231 | } 232 | 233 | log.Printf("Qemu version: %s", matches[0]) 234 | return matches[0], nil 235 | } 236 | 237 | func logReader(name string, r io.Reader) { 238 | bufR := bufio.NewReader(r) 239 | for { 240 | line, err := bufR.ReadString('\n') 241 | if line != "" { 242 | line = strings.TrimRightFunc(line, unicode.IsSpace) 243 | log.Printf("%s: %s", name, line) 244 | } 245 | 246 | if err == io.EOF { 247 | break 248 | } 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /builder/qemu/driver_mock.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package qemu 5 | 6 | import "sync" 7 | 8 | type DriverMock struct { 9 | sync.Mutex 10 | 11 | CopyCalled bool 12 | CopyErr error 13 | 14 | StopCalled bool 15 | StopErr error 16 | 17 | QemuCalls [][]string 18 | QemuErrs []error 19 | 20 | WaitForShutdownCalled bool 21 | WaitForShutdownState bool 22 | 23 | QemuImgCalled bool 24 | QemuImgCalls []string 25 | QemuImgErrs []error 26 | 27 | VerifyCalled bool 28 | VerifyErr error 29 | 30 | VersionCalled bool 31 | VersionResult string 32 | VersionErr error 33 | } 34 | 35 | func (d *DriverMock) Copy(source, dst string) error { 36 | d.CopyCalled = true 37 | return d.CopyErr 38 | } 39 | 40 | func (d *DriverMock) Stop() error { 41 | d.StopCalled = true 42 | return d.StopErr 43 | } 44 | 45 | func (d *DriverMock) Qemu(args ...string) error { 46 | d.QemuCalls = append(d.QemuCalls, args) 47 | 48 | if len(d.QemuErrs) >= len(d.QemuCalls) { 49 | return d.QemuErrs[len(d.QemuCalls)-1] 50 | } 51 | return nil 52 | } 53 | 54 | func (d *DriverMock) WaitForShutdown(cancelCh <-chan struct{}) bool { 55 | d.WaitForShutdownCalled = true 56 | return d.WaitForShutdownState 57 | } 58 | 59 | func (d *DriverMock) QemuImg(args ...string) error { 60 | d.QemuImgCalled = true 61 | d.QemuImgCalls = append(d.QemuImgCalls, args...) 62 | 63 | if len(d.QemuImgErrs) >= len(d.QemuImgCalls) { 64 | return d.QemuImgErrs[len(d.QemuImgCalls)-1] 65 | } 66 | return nil 67 | } 68 | 69 | func (d *DriverMock) Verify() error { 70 | d.VerifyCalled = true 71 | return d.VerifyErr 72 | } 73 | 74 | func (d *DriverMock) Version() (string, error) { 75 | d.VersionCalled = true 76 | return d.VersionResult, d.VersionErr 77 | } 78 | -------------------------------------------------------------------------------- /builder/qemu/qmp.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package qemu 5 | 6 | import ( 7 | "encoding/json" 8 | "fmt" 9 | "strings" 10 | 11 | "github.com/digitalocean/go-qemu/qmp" 12 | ) 13 | 14 | type qomListRequest struct { 15 | Execute string `json:"execute"` 16 | Arguments qomListRequestArguments `json:"arguments"` 17 | } 18 | 19 | type qomListRequestArguments struct { 20 | Path string `json:"path"` 21 | } 22 | 23 | type qomListResponse struct { 24 | Return []qomListReturn `json:"return"` 25 | } 26 | 27 | type qomListReturn struct { 28 | Name string `json:"name"` 29 | Type string `json:"type"` 30 | } 31 | 32 | func qmpQomList(qmpMonitor *qmp.SocketMonitor, path string) ([]qomListReturn, error) { 33 | request, _ := json.Marshal(qomListRequest{ 34 | Execute: "qom-list", 35 | Arguments: qomListRequestArguments{ 36 | Path: path, 37 | }, 38 | }) 39 | result, err := qmpMonitor.Run(request) 40 | if err != nil { 41 | return nil, err 42 | } 43 | var response qomListResponse 44 | if err := json.Unmarshal(result, &response); err != nil { 45 | return nil, err 46 | } 47 | return response.Return, nil 48 | } 49 | 50 | type qomGetRequest struct { 51 | Execute string `json:"execute"` 52 | Arguments qomGetRequestArguments `json:"arguments"` 53 | } 54 | 55 | type qomGetRequestArguments struct { 56 | Path string `json:"path"` 57 | Property string `json:"property"` 58 | } 59 | 60 | type qomGetResponse struct { 61 | Return string `json:"return"` 62 | } 63 | 64 | func qmpQomGet(qmpMonitor *qmp.SocketMonitor, path string, property string) (string, error) { 65 | request, _ := json.Marshal(qomGetRequest{ 66 | Execute: "qom-get", 67 | Arguments: qomGetRequestArguments{ 68 | Path: path, 69 | Property: property, 70 | }, 71 | }) 72 | result, err := qmpMonitor.Run(request) 73 | if err != nil { 74 | return "", err 75 | } 76 | var response qomGetResponse 77 | if err := json.Unmarshal(result, &response); err != nil { 78 | return "", err 79 | } 80 | return response.Return, nil 81 | } 82 | 83 | type netDevice struct { 84 | Path string 85 | Name string 86 | Type string 87 | MacAddress string 88 | } 89 | 90 | func getNetDevices(qmpMonitor *qmp.SocketMonitor) ([]netDevice, error) { 91 | devices := []netDevice{} 92 | for _, parentPath := range []string{"/machine/peripheral", "/machine/peripheral-anon"} { 93 | listResponse, err := qmpQomList(qmpMonitor, parentPath) 94 | if err != nil { 95 | return nil, fmt.Errorf("failed to get qmp qom list %v: %w", parentPath, err) 96 | } 97 | for _, p := range listResponse { 98 | if strings.HasPrefix(p.Type, "child<") { 99 | path := fmt.Sprintf("%s/%s", parentPath, p.Name) 100 | r, err := qmpQomList(qmpMonitor, path) 101 | if err != nil { 102 | return nil, fmt.Errorf("failed to get qmp qom list %v: %w", path, err) 103 | } 104 | isNetdev := false 105 | for _, d := range r { 106 | if d.Name == "netdev" { 107 | isNetdev = true 108 | break 109 | } 110 | } 111 | if isNetdev { 112 | device := netDevice{ 113 | Path: path, 114 | } 115 | for _, d := range r { 116 | if d.Name != "type" && d.Name != "netdev" && d.Name != "mac" { 117 | continue 118 | } 119 | value, err := qmpQomGet(qmpMonitor, path, d.Name) 120 | if err != nil { 121 | return nil, fmt.Errorf("failed to get qmp qom property %v %v: %w", path, d.Name, err) 122 | } 123 | switch d.Name { 124 | case "type": 125 | device.Type = value 126 | case "netdev": 127 | device.Name = value 128 | case "mac": 129 | device.MacAddress = value 130 | } 131 | } 132 | devices = append(devices, device) 133 | } 134 | } 135 | } 136 | } 137 | return devices, nil 138 | } 139 | -------------------------------------------------------------------------------- /builder/qemu/ssh.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package qemu 5 | 6 | import ( 7 | "log" 8 | 9 | "github.com/hashicorp/packer-plugin-sdk/multistep" 10 | ) 11 | 12 | func commHost(host string) func(multistep.StateBag) (string, error) { 13 | return func(state multistep.StateBag) (string, error) { 14 | if host != "" { 15 | log.Printf("Using host value: %s", host) 16 | return host, nil 17 | } 18 | 19 | if guestAddress, ok := state.Get("guestAddress").(string); ok { 20 | return guestAddress, nil 21 | } 22 | 23 | return "127.0.0.1", nil 24 | } 25 | } 26 | 27 | func commPort(state multistep.StateBag) (int, error) { 28 | commHostPort, ok := state.Get("commHostPort").(int) 29 | if !ok { 30 | commHostPort = 22 31 | } 32 | return commHostPort, nil 33 | } 34 | -------------------------------------------------------------------------------- /builder/qemu/step_configure_qmp.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package qemu 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "log" 10 | "os" 11 | "time" 12 | 13 | "github.com/digitalocean/go-qemu/qmp" 14 | "github.com/hashicorp/packer-plugin-sdk/multistep" 15 | packersdk "github.com/hashicorp/packer-plugin-sdk/packer" 16 | ) 17 | 18 | // This step configures the VM to enable the QMP listener. 19 | // 20 | // Uses: 21 | // 22 | // config *config 23 | // ui packersdk.Ui 24 | // 25 | // Produces: 26 | type stepConfigureQMP struct { 27 | monitor *qmp.SocketMonitor 28 | QMPSocketPath string 29 | } 30 | 31 | func (s *stepConfigureQMP) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { 32 | config := state.Get("config").(*Config) 33 | ui := state.Get("ui").(packersdk.Ui) 34 | 35 | if !config.QMPEnable { 36 | return multistep.ActionContinue 37 | } 38 | 39 | ui.Say(fmt.Sprintf("QMP socket at: %s", s.QMPSocketPath)) 40 | 41 | // Only initialize and open QMP when we have a use for it. 42 | // Open QMP socket 43 | var err error 44 | var cmd []byte 45 | var result []byte 46 | s.monitor, err = qmp.NewSocketMonitor("unix", s.QMPSocketPath, 2*time.Second) 47 | if err != nil { 48 | err := fmt.Errorf("Error opening QMP socket: %s", err) 49 | state.Put("error", err) 50 | ui.Error(err.Error()) 51 | return multistep.ActionHalt 52 | } 53 | 54 | // Connect to QMP 55 | // function automatically calls capabilities so is immediately ready for commands 56 | err = s.monitor.Connect() 57 | if err != nil { 58 | err := fmt.Errorf("Error connecting to QMP socket: %s", err) 59 | state.Put("error", err) 60 | ui.Error(err.Error()) 61 | return multistep.ActionHalt 62 | } 63 | log.Printf("QMP socket open SUCCESS") 64 | 65 | vncPassword := state.Get("vnc_password") 66 | if vncPassword != "" { 67 | cmd = []byte(fmt.Sprintf("{ \"execute\": \"change-vnc-password\", \"arguments\": { \"password\": \"%s\" } }", 68 | vncPassword)) 69 | result, err = s.monitor.Run(cmd) 70 | if err != nil { 71 | err := fmt.Errorf("Error connecting to QMP socket: %s", err) 72 | state.Put("error", err) 73 | ui.Error(err.Error()) 74 | return multistep.ActionHalt 75 | } 76 | log.Printf("QMP Command: %s\nResult: %s", cmd, result) 77 | } 78 | 79 | // make the qmp_monitor available to other steps. 80 | state.Put("qmp_monitor", s.monitor) 81 | 82 | return multistep.ActionContinue 83 | } 84 | 85 | func (s *stepConfigureQMP) Cleanup(multistep.StateBag) { 86 | if s.monitor != nil { 87 | err := s.monitor.Disconnect() 88 | if err != nil { 89 | log.Printf("failed to disconnect QMP: %v", err) 90 | } 91 | // Delete file associated with qmp socket. 92 | if err := os.Remove(s.QMPSocketPath); err != nil { 93 | log.Printf("Failed to delete the qmp socket file: %s", err) 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /builder/qemu/step_configure_vnc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package qemu 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "log" 10 | "math/rand" 11 | 12 | "github.com/hashicorp/packer-plugin-sdk/multistep" 13 | "github.com/hashicorp/packer-plugin-sdk/net" 14 | packersdk "github.com/hashicorp/packer-plugin-sdk/packer" 15 | ) 16 | 17 | // This step configures the VM to enable the VNC server. 18 | // 19 | // Uses: 20 | // 21 | // config *config 22 | // ui packersdk.Ui 23 | // 24 | // Produces: 25 | // 26 | // vnc_port int - The port that VNC is configured to listen on. 27 | type stepConfigureVNC struct { 28 | l *net.Listener 29 | } 30 | 31 | func VNCPassword(c *Config) (string, error) { 32 | if !c.VNCUsePassword { 33 | return "", nil 34 | } 35 | 36 | if len(c.VNCPassword) > 8 { 37 | return "", fmt.Errorf("password length is longer than expected %d > %d", len(c.VNCPassword), 8) 38 | } 39 | 40 | if len(c.VNCPassword) != 0 { 41 | return c.VNCPassword, nil 42 | } 43 | 44 | length := int(8) 45 | 46 | charSet := []byte("012345689abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") 47 | charSetLength := len(charSet) 48 | 49 | password := make([]byte, length) 50 | 51 | for i := 0; i < length; i++ { 52 | password[i] = charSet[rand.Intn(charSetLength)] 53 | } 54 | 55 | return string(password), nil 56 | } 57 | 58 | func (s *stepConfigureVNC) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { 59 | config := state.Get("config").(*Config) 60 | ui := state.Get("ui").(packersdk.Ui) 61 | 62 | // Find an open VNC port. Note that this can still fail later on 63 | // because we have to release the port at some point. But this does its 64 | // best. 65 | msg := fmt.Sprintf("Looking for available port between %d and %d on %s", config.VNCPortMin, config.VNCPortMax, config.VNCBindAddress) 66 | ui.Say(msg) 67 | log.Print(msg) 68 | 69 | var vncPassword string 70 | var err error 71 | s.l, err = net.ListenRangeConfig{ 72 | Addr: config.VNCBindAddress, 73 | Min: config.VNCPortMin, 74 | Max: config.VNCPortMax, 75 | Network: "tcp", 76 | }.Listen(ctx) 77 | if err != nil { 78 | err := fmt.Errorf("Error finding VNC port: %s", err) 79 | state.Put("error", err) 80 | ui.Error(err.Error()) 81 | return multistep.ActionHalt 82 | } 83 | s.l.Listener.Close() // free port, but don't unlock lock file 84 | vncPort := s.l.Port 85 | 86 | vncPassword, err = VNCPassword(config) 87 | if err != nil { 88 | err := fmt.Errorf("Error finding VNC password: %s", err) 89 | state.Put("error", err) 90 | ui.Error(err.Error()) 91 | return multistep.ActionHalt 92 | } 93 | 94 | log.Printf("Found available VNC port: %d on IP: %s", vncPort, config.VNCBindAddress) 95 | state.Put("vnc_port", vncPort) 96 | state.Put("vnc_password", vncPassword) 97 | 98 | return multistep.ActionContinue 99 | } 100 | 101 | func (s *stepConfigureVNC) Cleanup(multistep.StateBag) { 102 | if s.l != nil { 103 | err := s.l.Close() 104 | if err != nil { 105 | log.Printf("failed to unlock port lockfile: %v", err) 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /builder/qemu/step_convert_disk.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package qemu 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "path/filepath" 10 | "strings" 11 | "time" 12 | 13 | "github.com/hashicorp/packer-plugin-sdk/multistep" 14 | packersdk "github.com/hashicorp/packer-plugin-sdk/packer" 15 | "github.com/hashicorp/packer-plugin-sdk/retry" 16 | 17 | "os" 18 | ) 19 | 20 | // This step converts the virtual disk that was used as the 21 | // hard drive for the virtual machine. 22 | type stepConvertDisk struct { 23 | DiskCompression bool 24 | Format string 25 | OutputDir string 26 | SkipCompaction bool 27 | VMName string 28 | 29 | QemuImgArgs QemuImgArgs 30 | } 31 | 32 | func (s *stepConvertDisk) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { 33 | driver := state.Get("driver").(Driver) 34 | ui := state.Get("ui").(packersdk.Ui) 35 | 36 | diskName := s.VMName 37 | 38 | if s.SkipCompaction && !s.DiskCompression { 39 | return multistep.ActionContinue 40 | } 41 | 42 | name := diskName + ".convert" 43 | 44 | sourcePath := filepath.Join(s.OutputDir, diskName) 45 | targetPath := filepath.Join(s.OutputDir, name) 46 | 47 | command := s.buildConvertCommand(sourcePath, targetPath) 48 | 49 | ui.Say("Converting hard drive...") 50 | // Retry the conversion a few times in case it takes the qemu process a 51 | // moment to release the lock 52 | err := retry.Config{ 53 | Tries: 10, 54 | ShouldRetry: func(err error) bool { 55 | if strings.Contains(err.Error(), `Failed to get shared "write" lock`) { 56 | ui.Say("Error getting file lock for conversion; retrying...") 57 | return true 58 | } 59 | return false 60 | }, 61 | RetryDelay: (&retry.Backoff{InitialBackoff: 1 * time.Second, MaxBackoff: 10 * time.Second, Multiplier: 2}).Linear, 62 | }.Run(ctx, func(ctx context.Context) error { 63 | return driver.QemuImg(command...) 64 | }) 65 | 66 | if err != nil { 67 | switch err.(type) { 68 | case *retry.RetryExhaustedError: 69 | err = fmt.Errorf("Exhausted retries for getting file lock: %s", err) 70 | state.Put("error", err) 71 | ui.Error(err.Error()) 72 | return multistep.ActionHalt 73 | default: 74 | err := fmt.Errorf("Error converting hard drive: %s", err) 75 | state.Put("error", err) 76 | ui.Error(err.Error()) 77 | return multistep.ActionHalt 78 | } 79 | } 80 | 81 | if err := os.Rename(targetPath, sourcePath); err != nil { 82 | err := fmt.Errorf("Error moving converted hard drive: %s", err) 83 | state.Put("error", err) 84 | ui.Error(err.Error()) 85 | return multistep.ActionHalt 86 | } 87 | 88 | return multistep.ActionContinue 89 | } 90 | 91 | func (s *stepConvertDisk) buildConvertCommand(sourcePath, targetPath string) []string { 92 | command := []string{"convert"} 93 | 94 | if s.DiskCompression { 95 | command = append(command, "-c") 96 | } 97 | 98 | // Add user-provided convert args 99 | command = append(command, s.QemuImgArgs.Convert...) 100 | 101 | // Add format, and paths. 102 | command = append(command, "-O", s.Format, sourcePath, targetPath) 103 | 104 | return command 105 | } 106 | 107 | func (s *stepConvertDisk) Cleanup(state multistep.StateBag) {} 108 | -------------------------------------------------------------------------------- /builder/qemu/step_convert_disk_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package qemu 5 | 6 | import ( 7 | "fmt" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func Test_buildConvertCommand(t *testing.T) { 14 | type testCase struct { 15 | Step *stepConvertDisk 16 | Expected []string 17 | Reason string 18 | } 19 | testcases := []testCase{ 20 | { 21 | &stepConvertDisk{ 22 | Format: "qcow2", 23 | DiskCompression: false, 24 | }, 25 | []string{"convert", "-O", "qcow2", "source.qcow", "target.qcow2"}, 26 | "Basic, happy path, no compression, no extra args", 27 | }, 28 | { 29 | &stepConvertDisk{ 30 | Format: "qcow2", 31 | DiskCompression: true, 32 | }, 33 | []string{"convert", "-c", "-O", "qcow2", "source.qcow", "target.qcow2"}, 34 | "Basic, happy path, with compression, no extra args", 35 | }, 36 | { 37 | &stepConvertDisk{ 38 | Format: "qcow2", 39 | DiskCompression: true, 40 | QemuImgArgs: QemuImgArgs{ 41 | Convert: []string{"-o", "preallocation=full"}, 42 | }, 43 | }, 44 | []string{"convert", "-c", "-o", "preallocation=full", "-O", "qcow2", "source.qcow", "target.qcow2"}, 45 | "Basic, happy path, with compression, one set of extra args", 46 | }, 47 | } 48 | 49 | for _, tc := range testcases { 50 | command := tc.Step.buildConvertCommand("source.qcow", "target.qcow2") 51 | 52 | assert.Equal(t, command, tc.Expected, 53 | fmt.Sprintf("%s. Expected %#v", tc.Reason, tc.Expected)) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /builder/qemu/step_copy_disk.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package qemu 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "path/filepath" 10 | 11 | "github.com/hashicorp/packer-plugin-sdk/multistep" 12 | packersdk "github.com/hashicorp/packer-plugin-sdk/packer" 13 | ) 14 | 15 | // This step copies the virtual disk that will be used as the 16 | // hard drive for the virtual machine. 17 | type stepCopyDisk struct { 18 | DiskImage bool 19 | Format string 20 | OutputDir string 21 | UseBackingFile bool 22 | VMName string 23 | 24 | QemuImgArgs QemuImgArgs 25 | } 26 | 27 | func (s *stepCopyDisk) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { 28 | driver := state.Get("driver").(Driver) 29 | isoPath := state.Get("iso_path").(string) 30 | ui := state.Get("ui").(packersdk.Ui) 31 | path := filepath.Join(s.OutputDir, s.VMName) 32 | 33 | // Only make a copy of the ISO as the 'main' or 'default' disk if is a disk 34 | // image and not using a backing file. The create disk step (step_create_disk.go) 35 | // would already have made the disk otherwise 36 | if !s.DiskImage || s.UseBackingFile { 37 | return multistep.ActionContinue 38 | } 39 | 40 | // In some cases, the file formats provided are equivalent by comparing the 41 | // file extensions. Skip the conversion step 42 | // This also serves as a workaround for a QEMU bug: https://bugs.launchpad.net/qemu/+bug/1776920 43 | ext := filepath.Ext(isoPath) 44 | if len(ext) >= 1 && ext[1:] == s.Format && len(s.QemuImgArgs.Convert) == 0 { 45 | ui.Message("File extension already matches desired output format. " + 46 | "Skipping qemu-img convert step") 47 | err := driver.Copy(isoPath, path) 48 | if err != nil { 49 | state.Put("error", err) 50 | ui.Error(err.Error()) 51 | return multistep.ActionHalt 52 | } 53 | return multistep.ActionContinue 54 | } 55 | 56 | command := s.buildConvertCommand(isoPath, path) 57 | 58 | ui.Say("Copying hard drive...") 59 | if err := driver.QemuImg(command...); err != nil { 60 | err := fmt.Errorf("Error creating hard drive: %s", err) 61 | state.Put("error", err) 62 | ui.Error(err.Error()) 63 | return multistep.ActionHalt 64 | } 65 | 66 | return multistep.ActionContinue 67 | } 68 | 69 | func (s *stepCopyDisk) buildConvertCommand(sourcePath, targetPath string) []string { 70 | command := []string{"convert"} 71 | 72 | // Add user-provided convert args 73 | command = append(command, s.QemuImgArgs.Convert...) 74 | 75 | // Add format, and paths. 76 | command = append(command, "-O", s.Format, sourcePath, targetPath) 77 | 78 | return command 79 | } 80 | 81 | func (s *stepCopyDisk) Cleanup(state multistep.StateBag) {} 82 | -------------------------------------------------------------------------------- /builder/qemu/step_copy_disk_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package qemu 5 | 6 | import ( 7 | "context" 8 | "testing" 9 | 10 | "github.com/hashicorp/packer-plugin-sdk/multistep" 11 | packersdk "github.com/hashicorp/packer-plugin-sdk/packer" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func copyTestState(t *testing.T, d *DriverMock) multistep.StateBag { 16 | state := new(multistep.BasicStateBag) 17 | state.Put("ui", packersdk.TestUi(t)) 18 | state.Put("driver", d) 19 | state.Put("iso_path", "example_source.qcow2") 20 | 21 | return state 22 | } 23 | 24 | func Test_StepCopySkip(t *testing.T) { 25 | testcases := []stepCopyDisk{ 26 | stepCopyDisk{ 27 | DiskImage: false, 28 | UseBackingFile: false, 29 | }, 30 | stepCopyDisk{ 31 | DiskImage: true, 32 | UseBackingFile: true, 33 | }, 34 | stepCopyDisk{ 35 | DiskImage: false, 36 | UseBackingFile: true, 37 | }, 38 | } 39 | 40 | for _, tc := range testcases { 41 | d := new(DriverMock) 42 | state := copyTestState(t, d) 43 | action := tc.Run(context.TODO(), state) 44 | if action != multistep.ActionContinue { 45 | t.Fatalf("Should have gotten an ActionContinue") 46 | } 47 | 48 | if d.CopyCalled || d.QemuImgCalled { 49 | t.Fatalf("Should have skipped step since DiskImage and UseBackingFile are not set") 50 | } 51 | } 52 | } 53 | 54 | func Test_StepCopyCalled(t *testing.T) { 55 | step := stepCopyDisk{ 56 | DiskImage: true, 57 | Format: "qcow2", 58 | VMName: "output.qcow2", 59 | } 60 | 61 | d := new(DriverMock) 62 | state := copyTestState(t, d) 63 | action := step.Run(context.TODO(), state) 64 | if action != multistep.ActionContinue { 65 | t.Fatalf("Should have gotten an ActionContinue") 66 | } 67 | 68 | if !d.CopyCalled { 69 | t.Fatalf("Should have copied since all extensions are qcow2") 70 | } 71 | if d.QemuImgCalled { 72 | t.Fatalf("Should not have called qemu-img when formats match") 73 | } 74 | } 75 | 76 | func Test_StepQemuImgCalled(t *testing.T) { 77 | step := stepCopyDisk{ 78 | DiskImage: true, 79 | Format: "raw", 80 | VMName: "output.qcow2", 81 | } 82 | 83 | d := new(DriverMock) 84 | state := copyTestState(t, d) 85 | action := step.Run(context.TODO(), state) 86 | if action != multistep.ActionContinue { 87 | t.Fatalf("Should have gotten an ActionContinue") 88 | } 89 | if d.CopyCalled { 90 | t.Fatalf("Should not have copied since extensions don't match") 91 | } 92 | if !d.QemuImgCalled { 93 | t.Fatalf("Should have called qemu-img since extensions don't match") 94 | } 95 | } 96 | 97 | func Test_StepQemuImgCalledWithExtraArgs(t *testing.T) { 98 | step := &stepCopyDisk{ 99 | DiskImage: true, 100 | Format: "raw", 101 | VMName: "output.qcow2", 102 | QemuImgArgs: QemuImgArgs{ 103 | Convert: []string{"-o", "preallocation=full"}, 104 | }, 105 | } 106 | 107 | d := new(DriverMock) 108 | state := copyTestState(t, d) 109 | action := step.Run(context.TODO(), state) 110 | if action != multistep.ActionContinue { 111 | t.Fatalf("Should have gotten an ActionContinue") 112 | } 113 | if d.CopyCalled { 114 | t.Fatalf("Should not have copied since extensions don't match") 115 | } 116 | if !d.QemuImgCalled { 117 | t.Fatalf("Should have called qemu-img since extensions don't match") 118 | } 119 | assert.Equal( 120 | t, 121 | d.QemuImgCalls, 122 | []string{"convert", "-o", "preallocation=full", "-O", "raw", 123 | "example_source.qcow2", "output.qcow2"}, 124 | "should have added user extra args") 125 | } 126 | -------------------------------------------------------------------------------- /builder/qemu/step_create_disk.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package qemu 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "log" 10 | "path/filepath" 11 | 12 | "github.com/hashicorp/packer-plugin-sdk/multistep" 13 | packersdk "github.com/hashicorp/packer-plugin-sdk/packer" 14 | ) 15 | 16 | // This step creates the virtual disk that will be used as the 17 | // hard drive for the virtual machine. 18 | type stepCreateDisk struct { 19 | AdditionalDiskSize []string 20 | DiskImage bool 21 | DiskSize string 22 | Format string 23 | OutputDir string 24 | UseBackingFile bool 25 | VMName string 26 | QemuImgArgs QemuImgArgs 27 | } 28 | 29 | func (s *stepCreateDisk) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { 30 | driver := state.Get("driver").(Driver) 31 | ui := state.Get("ui").(packersdk.Ui) 32 | name := s.VMName 33 | 34 | if len(s.AdditionalDiskSize) > 0 || s.UseBackingFile { 35 | ui.Say("Creating required virtual machine disks") 36 | } 37 | 38 | // The 'main' or 'default' disk 39 | diskFullPaths := []string{filepath.Join(s.OutputDir, name)} 40 | diskSizes := []string{s.DiskSize} 41 | 42 | // Additional disks 43 | for i, diskSize := range s.AdditionalDiskSize { 44 | path := filepath.Join(s.OutputDir, fmt.Sprintf("%s-%d", name, i+1)) 45 | diskFullPaths = append(diskFullPaths, path) 46 | diskSizes = append(diskSizes, diskSize) 47 | } 48 | 49 | // Create all required disks 50 | for i, diskFullPath := range diskFullPaths { 51 | if s.DiskImage && !s.UseBackingFile && i == 0 { 52 | // Let the copy disk step (step_copy_disk.go) create the 'main' or 53 | // 'default' disk. 54 | continue 55 | } 56 | log.Printf("[INFO] Creating disk with Path: %s and Size: %s", diskFullPath, diskSizes[i]) 57 | 58 | command := s.buildCreateCommand(diskFullPath, diskSizes[i], i, state) 59 | 60 | if err := driver.QemuImg(command...); err != nil { 61 | err := fmt.Errorf("Error creating hard drive: %s", err) 62 | state.Put("error", err) 63 | ui.Error(err.Error()) 64 | return multistep.ActionHalt 65 | } 66 | } 67 | 68 | // Stash the disk paths so we can retrieve later 69 | state.Put("qemu_disk_paths", diskFullPaths) 70 | 71 | return multistep.ActionContinue 72 | } 73 | 74 | func (s *stepCreateDisk) buildCreateCommand(path string, size string, i int, state multistep.StateBag) []string { 75 | command := []string{"create", "-f", s.Format} 76 | 77 | if s.DiskImage && s.UseBackingFile && i == 0 { 78 | // Use a backing file for the 'main' or 'default' disk 79 | isoPath := state.Get("iso_path").(string) 80 | command = append(command, "-b", isoPath, "-F", "qcow2") 81 | } 82 | 83 | // add user-provided convert args 84 | command = append(command, s.QemuImgArgs.Create...) 85 | 86 | // add target path and size. 87 | command = append(command, path, size) 88 | 89 | return command 90 | } 91 | 92 | func (s *stepCreateDisk) Cleanup(state multistep.StateBag) {} 93 | -------------------------------------------------------------------------------- /builder/qemu/step_create_disk_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package qemu 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "testing" 10 | 11 | "github.com/hashicorp/packer-plugin-sdk/multistep" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func Test_buildCreateCommand(t *testing.T) { 16 | type testCase struct { 17 | Step *stepCreateDisk 18 | I int 19 | Expected []string 20 | Reason string 21 | } 22 | testcases := []testCase{ 23 | { 24 | &stepCreateDisk{ 25 | Format: "qcow2", 26 | UseBackingFile: false, 27 | }, 28 | 0, 29 | []string{"create", "-f", "qcow2", "target.qcow2", "1234M"}, 30 | "Basic, happy path, no backing store, no extra args", 31 | }, 32 | { 33 | &stepCreateDisk{ 34 | Format: "qcow2", 35 | DiskImage: true, 36 | UseBackingFile: true, 37 | AdditionalDiskSize: []string{"1M", "2M"}, 38 | }, 39 | 0, 40 | []string{"create", "-f", "qcow2", "-b", "source.qcow2", "-F", "qcow2", "target.qcow2", "1234M"}, 41 | "Basic, happy path, backing store, additional disks", 42 | }, 43 | { 44 | &stepCreateDisk{ 45 | Format: "qcow2", 46 | UseBackingFile: true, 47 | DiskImage: true, 48 | }, 49 | 1, 50 | []string{"create", "-f", "qcow2", "target.qcow2", "1234M"}, 51 | "Basic, happy path, backing store set but not at first index, no extra args", 52 | }, 53 | { 54 | &stepCreateDisk{ 55 | Format: "qcow2", 56 | UseBackingFile: true, 57 | DiskImage: true, 58 | QemuImgArgs: QemuImgArgs{ 59 | Create: []string{"-foo", "bar"}, 60 | }, 61 | }, 62 | 0, 63 | []string{"create", "-f", "qcow2", "-b", "source.qcow2", "-F", "qcow2", "-foo", "bar", "target.qcow2", "1234M"}, 64 | "Basic, happy path, backing store set, extra args", 65 | }, 66 | { 67 | &stepCreateDisk{ 68 | Format: "qcow2", 69 | UseBackingFile: true, 70 | QemuImgArgs: QemuImgArgs{ 71 | Create: []string{"-foo", "bar"}, 72 | }, 73 | }, 74 | 1, 75 | []string{"create", "-f", "qcow2", "-foo", "bar", "target.qcow2", "1234M"}, 76 | "Basic, happy path, backing store set but not at first index, extra args", 77 | }, 78 | } 79 | 80 | for _, tc := range testcases { 81 | state := new(multistep.BasicStateBag) 82 | state.Put("iso_path", "source.qcow2") 83 | command := tc.Step.buildCreateCommand("target.qcow2", "1234M", tc.I, state) 84 | 85 | assert.Equal(t, command, tc.Expected, 86 | fmt.Sprintf("%s. Expected %#v", tc.Reason, tc.Expected)) 87 | } 88 | } 89 | 90 | func Test_StepCreateCalled(t *testing.T) { 91 | type testCase struct { 92 | Step *stepCreateDisk 93 | Expected []string 94 | Reason string 95 | } 96 | testcases := []testCase{ 97 | { 98 | &stepCreateDisk{ 99 | Format: "qcow2", 100 | DiskImage: true, 101 | DiskSize: "1M", 102 | VMName: "target", 103 | UseBackingFile: true, 104 | }, 105 | []string{ 106 | "create", "-f", "qcow2", "-b", "source.qcow2", "-F", "qcow2", "target", "1M", 107 | }, 108 | "Basic, happy path, backing store, no additional disks", 109 | }, 110 | { 111 | &stepCreateDisk{ 112 | Format: "raw", 113 | DiskImage: false, 114 | DiskSize: "4M", 115 | VMName: "target", 116 | UseBackingFile: false, 117 | }, 118 | []string{ 119 | "create", "-f", "raw", "target", "4M", 120 | }, 121 | "Basic, happy path, raw, no additional disks", 122 | }, 123 | { 124 | &stepCreateDisk{ 125 | Format: "qcow2", 126 | DiskImage: true, 127 | DiskSize: "4M", 128 | VMName: "target", 129 | UseBackingFile: false, 130 | AdditionalDiskSize: []string{"3M", "8M"}, 131 | }, 132 | []string{ 133 | "create", "-f", "qcow2", "target-1", "3M", 134 | "create", "-f", "qcow2", "target-2", "8M", 135 | }, 136 | "Skips disk creation when disk can be copied", 137 | }, 138 | { 139 | &stepCreateDisk{ 140 | Format: "qcow2", 141 | DiskImage: true, 142 | DiskSize: "4M", 143 | VMName: "target", 144 | UseBackingFile: false, 145 | }, 146 | nil, 147 | "Skips disk creation when disk can be copied", 148 | }, 149 | { 150 | &stepCreateDisk{ 151 | Format: "qcow2", 152 | DiskImage: true, 153 | DiskSize: "1M", 154 | VMName: "target", 155 | UseBackingFile: true, 156 | AdditionalDiskSize: []string{"3M", "8M"}, 157 | }, 158 | []string{ 159 | "create", "-f", "qcow2", "-b", "source.qcow2", "-F", "qcow2", "target", "1M", 160 | "create", "-f", "qcow2", "target-1", "3M", 161 | "create", "-f", "qcow2", "target-2", "8M", 162 | }, 163 | "Basic, happy path, backing store, additional disks", 164 | }, 165 | } 166 | 167 | for _, tc := range testcases { 168 | d := new(DriverMock) 169 | state := copyTestState(t, d) 170 | state.Put("iso_path", "source.qcow2") 171 | action := tc.Step.Run(context.TODO(), state) 172 | if action != multistep.ActionContinue { 173 | t.Fatalf("Should have gotten an ActionContinue") 174 | } 175 | 176 | assert.Equal(t, d.QemuImgCalls, tc.Expected, 177 | fmt.Sprintf("%s. Expected %#v", tc.Reason, tc.Expected)) 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /builder/qemu/step_create_vtpm.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package qemu 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "log" 10 | "os" 11 | "os/exec" 12 | "runtime" 13 | 14 | "github.com/hashicorp/packer-plugin-sdk/multistep" 15 | packersdk "github.com/hashicorp/packer-plugin-sdk/packer" 16 | ) 17 | 18 | type stepCreatevTPM struct { 19 | enableVTPM bool 20 | vtpmType string 21 | isTPM1 bool 22 | } 23 | 24 | const ( 25 | qemuVTPM string = "qemu_vtpm" 26 | swtpmProcess string = "qemu_swtpm_process" 27 | swtpmTmpDir string = "qemu_swtpm_dir" 28 | swtpmSocketPath string = "qemu_swtpm_socket_path" 29 | ) 30 | 31 | func (s *stepCreatevTPM) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { 32 | if !s.enableVTPM { 33 | return multistep.ActionContinue 34 | } 35 | 36 | ui := state.Get("ui").(packersdk.Ui) 37 | 38 | if runtime.GOOS == "windows" { 39 | ui.Error("vTPM is only supported on UNIX OSes for now") 40 | return multistep.ActionHalt 41 | } 42 | 43 | swtpmPath, err := exec.LookPath("swtpm") 44 | if err != nil { 45 | ui.Error(fmt.Sprintf( 46 | "failed to locate swtpm (%s), this is required for vTPM support", 47 | err)) 48 | return multistep.ActionHalt 49 | } 50 | 51 | vtpmDeviceDir, err := os.MkdirTemp("", "") 52 | if err != nil { 53 | ui.Error(fmt.Sprintf("failed to create vtpm state directory: %s", err)) 54 | return multistep.ActionHalt 55 | } 56 | 57 | state.Put(swtpmTmpDir, vtpmDeviceDir) 58 | 59 | sockPath := fmt.Sprintf("%s/vtpm.sock", vtpmDeviceDir) 60 | 61 | state.Put(swtpmSocketPath, sockPath) 62 | if err != nil { 63 | ui.Error(fmt.Sprintf( 64 | "failed to create swtpm communication: %s", 65 | err)) 66 | return multistep.ActionHalt 67 | } 68 | 69 | args := []string{ 70 | "socket", 71 | "--tpmstate", fmt.Sprintf("dir=%s", vtpmDeviceDir), 72 | "--ctrl", fmt.Sprintf("type=unixio,path=%s", sockPath), 73 | } 74 | 75 | if !s.isTPM1 { 76 | args = append(args, "--tpm2") 77 | } 78 | 79 | swtpm := exec.Command(swtpmPath, args...) 80 | swtpm.Stdout = os.Stdout 81 | swtpm.Stderr = os.Stderr 82 | 83 | state.Put(qemuVTPM, true) 84 | 85 | log.Printf("Executing swtpm: %+v", args) 86 | err = swtpm.Start() 87 | if err != nil { 88 | ui.Error(fmt.Sprintf( 89 | "failed to start swtpm: %s", err)) 90 | return multistep.ActionHalt 91 | } 92 | 93 | state.Put(swtpmProcess, swtpm.Process) 94 | 95 | return multistep.ActionContinue 96 | } 97 | 98 | func (s *stepCreatevTPM) Cleanup(state multistep.StateBag) { 99 | process, ok := state.GetOk(swtpmProcess) 100 | if !ok { 101 | return 102 | } 103 | 104 | log.Printf("killing swtpm with PID %d", process.(*os.Process).Pid) 105 | err := process.(*os.Process).Kill() 106 | if err != nil { 107 | log.Printf("failed to kill swtpm: %s", err) 108 | } 109 | 110 | tmpDir := state.Get(swtpmTmpDir).(string) 111 | os.RemoveAll(tmpDir) 112 | } 113 | -------------------------------------------------------------------------------- /builder/qemu/step_http_ip_discover.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package qemu 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "net" 10 | 11 | "github.com/hashicorp/packer-plugin-sdk/multistep" 12 | packersdk "github.com/hashicorp/packer-plugin-sdk/packer" 13 | ) 14 | 15 | // Step to discover the http ip 16 | // which guests use to reach the vm host 17 | // To make sure the IP is set before boot command and http server steps 18 | type stepHTTPIPDiscover struct{} 19 | 20 | func (s *stepHTTPIPDiscover) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { 21 | config := state.Get("config").(*Config) 22 | ui := state.Get("ui").(packersdk.Ui) 23 | 24 | hostIP := "" 25 | 26 | if config.NetBridge == "" { 27 | hostIP = "10.0.2.2" 28 | } else { 29 | bridgeInterface, err := net.InterfaceByName(config.NetBridge) 30 | if err != nil { 31 | err := fmt.Errorf("Error getting the bridge %s interface: %s", config.NetBridge, err) 32 | ui.Error(err.Error()) 33 | return multistep.ActionHalt 34 | } 35 | addrs, err := bridgeInterface.Addrs() 36 | if err != nil { 37 | err := fmt.Errorf("Error getting the bridge %s interface addresses: %s", config.NetBridge, err) 38 | ui.Error(err.Error()) 39 | return multistep.ActionHalt 40 | } 41 | for _, addr := range addrs { 42 | var ip net.IP 43 | switch v := addr.(type) { 44 | case *net.IPNet: 45 | ip = v.IP 46 | case *net.IPAddr: 47 | ip = v.IP 48 | } 49 | if ip == nil { 50 | continue 51 | } 52 | ip = ip.To4() 53 | if ip == nil { 54 | continue 55 | } 56 | hostIP = ip.String() 57 | break 58 | } 59 | if hostIP == "" { 60 | err := fmt.Errorf("Error getting an IPv4 address from the bridge %s: cannot find any IPv4 address", config.NetBridge) 61 | ui.Error(err.Error()) 62 | return multistep.ActionHalt 63 | } 64 | } 65 | 66 | state.Put("http_ip", hostIP) 67 | 68 | return multistep.ActionContinue 69 | } 70 | 71 | func (s *stepHTTPIPDiscover) Cleanup(state multistep.StateBag) {} 72 | -------------------------------------------------------------------------------- /builder/qemu/step_http_ip_discover_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package qemu 5 | 6 | import ( 7 | "bytes" 8 | "context" 9 | "testing" 10 | 11 | "github.com/hashicorp/packer-plugin-sdk/multistep" 12 | packersdk "github.com/hashicorp/packer-plugin-sdk/packer" 13 | ) 14 | 15 | func TestStepHTTPIPDiscover_Run(t *testing.T) { 16 | state := new(multistep.BasicStateBag) 17 | state.Put("ui", &packersdk.BasicUi{ 18 | Reader: new(bytes.Buffer), 19 | Writer: new(bytes.Buffer), 20 | }) 21 | config := &Config{} 22 | state.Put("config", config) 23 | step := new(stepHTTPIPDiscover) 24 | hostIp := "10.0.2.2" 25 | 26 | // Test the run 27 | if action := step.Run(context.Background(), state); action != multistep.ActionContinue { 28 | t.Fatalf("bad action: %#v", action) 29 | } 30 | if _, ok := state.GetOk("error"); ok { 31 | t.Fatal("should NOT have error") 32 | } 33 | httpIp := state.Get("http_ip").(string) 34 | if httpIp != hostIp { 35 | t.Fatalf("bad: Http ip is %s but was supposed to be %s", httpIp, hostIp) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /builder/qemu/step_port_forward.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package qemu 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "log" 10 | 11 | "github.com/hashicorp/packer-plugin-sdk/multistep" 12 | "github.com/hashicorp/packer-plugin-sdk/net" 13 | packersdk "github.com/hashicorp/packer-plugin-sdk/packer" 14 | ) 15 | 16 | // This step adds a NAT port forwarding definition so that SSH or WinRM is available 17 | // on the guest machine. 18 | type stepPortForward struct { 19 | CommunicatorType string 20 | NetBridge string 21 | 22 | l *net.Listener 23 | } 24 | 25 | func (s *stepPortForward) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { 26 | config := state.Get("config").(*Config) 27 | ui := state.Get("ui").(packersdk.Ui) 28 | 29 | if s.CommunicatorType == "none" { 30 | ui.Message("No communicator is set; skipping port forwarding setup.") 31 | return multistep.ActionContinue 32 | } 33 | if s.NetBridge != "" { 34 | ui.Message("net_bridge is set; skipping port forwarding setup.") 35 | return multistep.ActionContinue 36 | } 37 | 38 | commHostPort := config.CommConfig.Comm.Port() 39 | 40 | if config.CommConfig.SkipNatMapping { 41 | log.Printf("Skipping NAT port forwarding. Using communicator (SSH, WinRM, etc) port %d", commHostPort) 42 | state.Put("commHostPort", commHostPort) 43 | return multistep.ActionContinue 44 | } 45 | 46 | log.Printf("Looking for available communicator (SSH, WinRM, etc) port between %d and %d", config.CommConfig.HostPortMin, config.CommConfig.HostPortMax) 47 | var err error 48 | s.l, err = net.ListenRangeConfig{ 49 | Addr: config.VNCBindAddress, 50 | Min: config.CommConfig.HostPortMin, 51 | Max: config.CommConfig.HostPortMax, 52 | Network: "tcp", 53 | }.Listen(ctx) 54 | if err != nil { 55 | err := fmt.Errorf("Error finding port: %s", err) 56 | state.Put("error", err) 57 | ui.Error(err.Error()) 58 | return multistep.ActionHalt 59 | } 60 | s.l.Listener.Close() // free port, but don't unlock lock file 61 | commHostPort = s.l.Port 62 | ui.Say(fmt.Sprintf("Found port for communicator (SSH, WinRM, etc): %d.", commHostPort)) 63 | 64 | // Save the port we're using so that future steps can use it 65 | state.Put("commHostPort", commHostPort) 66 | 67 | return multistep.ActionContinue 68 | } 69 | 70 | func (s *stepPortForward) Cleanup(state multistep.StateBag) { 71 | if s.l != nil { 72 | err := s.l.Close() 73 | if err != nil { 74 | log.Printf("failed to unlock port lockfile: %v", err) 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /builder/qemu/step_prepare_efivars.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package qemu 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "io" 10 | "os" 11 | "path/filepath" 12 | 13 | "github.com/hashicorp/packer-plugin-sdk/multistep" 14 | packersdk "github.com/hashicorp/packer-plugin-sdk/packer" 15 | ) 16 | 17 | // stepPrepareEfivars copies the EFIVars file to the output, so we can boot 18 | // and use it as a RW flash drive 19 | type stepPrepareEfivars struct { 20 | EFIEnabled bool 21 | OutputDir string 22 | SourcePath string 23 | } 24 | 25 | const efivarStateKey string = "EFI_VARS_FILE_PATH" 26 | 27 | func (s *stepPrepareEfivars) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { 28 | ui := state.Get("ui").(packersdk.Ui) 29 | 30 | if !s.EFIEnabled { 31 | return multistep.ActionContinue 32 | } 33 | 34 | dstPath := filepath.Join(s.OutputDir, "efivars.fd") 35 | outFile, err := os.OpenFile(dstPath, os.O_CREATE|os.O_WRONLY, 0660) 36 | if err != nil { 37 | errMsg := fmt.Sprintf("failed to create local efivars file at %s: %s", dstPath, err) 38 | ui.Error(errMsg) 39 | return multistep.ActionHalt 40 | } 41 | defer outFile.Close() 42 | 43 | state.Put(efivarStateKey, dstPath) 44 | 45 | inFile, err := os.Open(s.SourcePath) 46 | if err != nil { 47 | errMsg := fmt.Sprintf("failed to read from efivars file at %s: %s", s.SourcePath, err) 48 | ui.Error(errMsg) 49 | return multistep.ActionHalt 50 | } 51 | 52 | _, err = io.Copy(outFile, inFile) 53 | if err != nil { 54 | errMsg := fmt.Sprintf("failed to copy efivars data: %s", err) 55 | ui.Error(errMsg) 56 | return multistep.ActionHalt 57 | } 58 | 59 | return multistep.ActionContinue 60 | } 61 | 62 | func (s *stepPrepareEfivars) Cleanup(state multistep.StateBag) { 63 | if !s.EFIEnabled { 64 | return 65 | } 66 | 67 | _, cancelled := state.GetOk(multistep.StateCancelled) 68 | _, halted := state.GetOk(multistep.StateHalted) 69 | 70 | if cancelled || halted { 71 | efiVarFile, ok := state.GetOk(efivarStateKey) 72 | // If the path isn't in state, we can assume it's not been created and 73 | // therefore we have nothing to cleanup 74 | if !ok { 75 | return 76 | } 77 | 78 | os.Remove(efiVarFile.(string)) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /builder/qemu/step_prepare_output_dir.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package qemu 5 | 6 | import ( 7 | "context" 8 | "log" 9 | "os" 10 | "time" 11 | 12 | "github.com/hashicorp/packer-plugin-sdk/multistep" 13 | packersdk "github.com/hashicorp/packer-plugin-sdk/packer" 14 | ) 15 | 16 | type stepPrepareOutputDir struct{} 17 | 18 | func (stepPrepareOutputDir) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { 19 | config := state.Get("config").(*Config) 20 | ui := state.Get("ui").(packersdk.Ui) 21 | 22 | if _, err := os.Stat(config.OutputDir); err == nil && config.PackerForce { 23 | ui.Say("Deleting previous output directory...") 24 | os.RemoveAll(config.OutputDir) 25 | } 26 | 27 | if err := os.MkdirAll(config.OutputDir, 0755); err != nil { 28 | state.Put("error", err) 29 | return multistep.ActionHalt 30 | } 31 | 32 | return multistep.ActionContinue 33 | } 34 | 35 | func (stepPrepareOutputDir) Cleanup(state multistep.StateBag) { 36 | _, cancelled := state.GetOk(multistep.StateCancelled) 37 | _, halted := state.GetOk(multistep.StateHalted) 38 | 39 | if cancelled || halted { 40 | config := state.Get("config").(*Config) 41 | ui := state.Get("ui").(packersdk.Ui) 42 | 43 | ui.Say("Deleting output directory...") 44 | for i := 0; i < 5; i++ { 45 | err := os.RemoveAll(config.OutputDir) 46 | if err == nil { 47 | break 48 | } 49 | 50 | log.Printf("Error removing output dir: %s", err) 51 | time.Sleep(2 * time.Second) 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /builder/qemu/step_resize_disk.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package qemu 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "path/filepath" 10 | 11 | "github.com/hashicorp/packer-plugin-sdk/multistep" 12 | packersdk "github.com/hashicorp/packer-plugin-sdk/packer" 13 | ) 14 | 15 | // This step resizes the virtual disk that will be used as the 16 | // hard drive for the virtual machine. 17 | type stepResizeDisk struct { 18 | DiskCompression bool 19 | DiskImage bool 20 | Format string 21 | OutputDir string 22 | SkipResizeDisk bool 23 | VMName string 24 | DiskSize string 25 | 26 | QemuImgArgs QemuImgArgs 27 | } 28 | 29 | func (s *stepResizeDisk) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { 30 | driver := state.Get("driver").(Driver) 31 | ui := state.Get("ui").(packersdk.Ui) 32 | path := filepath.Join(s.OutputDir, s.VMName) 33 | 34 | command := s.buildResizeCommand(path) 35 | 36 | if s.DiskImage == false || s.SkipResizeDisk == true { 37 | return multistep.ActionContinue 38 | } 39 | 40 | ui.Say("Resizing hard drive...") 41 | if err := driver.QemuImg(command...); err != nil { 42 | err := fmt.Errorf("Error creating hard drive: %s", err) 43 | state.Put("error", err) 44 | ui.Error(err.Error()) 45 | return multistep.ActionHalt 46 | } 47 | 48 | return multistep.ActionContinue 49 | } 50 | 51 | func (s *stepResizeDisk) buildResizeCommand(path string) []string { 52 | command := []string{"resize", "-f", s.Format} 53 | 54 | // add user-provided convert args 55 | command = append(command, s.QemuImgArgs.Resize...) 56 | 57 | // Add file and size 58 | command = append(command, path, s.DiskSize) 59 | 60 | return command 61 | } 62 | 63 | func (s *stepResizeDisk) Cleanup(state multistep.StateBag) {} 64 | -------------------------------------------------------------------------------- /builder/qemu/step_resize_disk_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package qemu 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "testing" 10 | 11 | "github.com/hashicorp/packer-plugin-sdk/multistep" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func TestStepResizeDisk_Skips(t *testing.T) { 16 | testConfigs := []*Config{ 17 | &Config{ 18 | DiskImage: false, 19 | SkipResizeDisk: false, 20 | }, 21 | &Config{ 22 | DiskImage: false, 23 | SkipResizeDisk: true, 24 | }, 25 | } 26 | for _, config := range testConfigs { 27 | state := testState(t) 28 | driver := state.Get("driver").(*DriverMock) 29 | 30 | state.Put("config", config) 31 | step := new(stepResizeDisk) 32 | 33 | // Test the run 34 | if action := step.Run(context.Background(), state); action != multistep.ActionContinue { 35 | t.Fatalf("bad action: %#v", action) 36 | } 37 | if _, ok := state.GetOk("error"); ok { 38 | t.Fatal("should NOT have error") 39 | } 40 | if len(driver.QemuImgCalls) > 0 { 41 | t.Fatal("should NOT have called qemu-img") 42 | } 43 | } 44 | } 45 | 46 | func Test_buildResizeCommand(t *testing.T) { 47 | type testCase struct { 48 | Step *stepResizeDisk 49 | Expected []string 50 | Reason string 51 | } 52 | testcases := []testCase{ 53 | { 54 | &stepResizeDisk{ 55 | Format: "qcow2", 56 | DiskSize: "1234M", 57 | }, 58 | []string{"resize", "-f", "qcow2", "source.qcow", "1234M"}, 59 | "no extra args", 60 | }, 61 | { 62 | &stepResizeDisk{ 63 | Format: "qcow2", 64 | DiskSize: "1234M", 65 | QemuImgArgs: QemuImgArgs{ 66 | Resize: []string{"-foo", "bar"}, 67 | }, 68 | }, 69 | []string{"resize", "-f", "qcow2", "-foo", "bar", "source.qcow", "1234M"}, 70 | "one set of extra args", 71 | }, 72 | } 73 | 74 | for _, tc := range testcases { 75 | command := tc.Step.buildResizeCommand("source.qcow") 76 | 77 | assert.Equal(t, command, tc.Expected, 78 | fmt.Sprintf("%s. Expected %#v", tc.Reason, tc.Expected)) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /builder/qemu/step_run.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package qemu 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "log" 10 | "path/filepath" 11 | "strings" 12 | 13 | "github.com/hashicorp/go-version" 14 | "github.com/hashicorp/packer-plugin-sdk/multistep" 15 | packersdk "github.com/hashicorp/packer-plugin-sdk/packer" 16 | "github.com/hashicorp/packer-plugin-sdk/template/interpolate" 17 | ) 18 | 19 | // stepRun runs the virtual machine 20 | type stepRun struct { 21 | DiskImage bool 22 | 23 | atLeastVersion2 bool 24 | ui packersdk.Ui 25 | } 26 | 27 | func (s *stepRun) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { 28 | config := state.Get("config").(*Config) 29 | driver := state.Get("driver").(Driver) 30 | s.ui = state.Get("ui").(packersdk.Ui) 31 | 32 | // Figure out version of qemu; store on step for later use 33 | rawVersion, err := driver.Version() 34 | if err != nil { 35 | err := fmt.Errorf("Error determining qemu version: %s", err) 36 | s.ui.Error(err.Error()) 37 | return multistep.ActionHalt 38 | } 39 | qemuVersion, err := version.NewVersion(rawVersion) 40 | if err != nil { 41 | err := fmt.Errorf("Error parsing qemu version: %s", err) 42 | s.ui.Error(err.Error()) 43 | return multistep.ActionHalt 44 | } 45 | v2 := version.Must(version.NewVersion("2.0")) 46 | 47 | s.atLeastVersion2 = qemuVersion.GreaterThanOrEqual(v2) 48 | 49 | // Generate the qemu command 50 | command, err := s.getCommandArgs(config, state) 51 | if err != nil { 52 | err := fmt.Errorf("Error processing QemuArgs: %s", err) 53 | s.ui.Error(err.Error()) 54 | return multistep.ActionHalt 55 | } 56 | 57 | // run the qemu command 58 | if err := driver.Qemu(command...); err != nil { 59 | err := fmt.Errorf("Error launching VM: %s", err) 60 | s.ui.Error(err.Error()) 61 | return multistep.ActionHalt 62 | } 63 | 64 | return multistep.ActionContinue 65 | } 66 | 67 | func (s *stepRun) Cleanup(state multistep.StateBag) { 68 | driver := state.Get("driver").(Driver) 69 | ui := state.Get("ui").(packersdk.Ui) 70 | 71 | if err := driver.Stop(); err != nil { 72 | ui.Error(fmt.Sprintf("Error shutting down VM: %s", err)) 73 | } 74 | } 75 | 76 | func (s *stepRun) getDefaultArgs(config *Config, state multistep.StateBag) map[string]interface{} { 77 | 78 | defaultArgs := make(map[string]interface{}) 79 | 80 | // Configure "boot" arguement 81 | // Run command is different depending whether we're booting from an 82 | // installation CD or a pre-baked image 83 | bootDrive := "once=d" 84 | message := "Starting VM, booting from CD-ROM" 85 | if s.DiskImage { 86 | bootDrive = "c" 87 | message = "Starting VM, booting disk image" 88 | } 89 | s.ui.Say(message) 90 | if !config.QemuEFIBootConfig.EnableEFI { 91 | defaultArgs["-boot"] = bootDrive 92 | } 93 | 94 | // configure "-qmp" arguments 95 | if config.QMPEnable { 96 | defaultArgs["-qmp"] = fmt.Sprintf("unix:%s,server,nowait", config.QMPSocketPath) 97 | } 98 | 99 | // configure "-name" arguments 100 | defaultArgs["-name"] = config.VMName 101 | 102 | // Configure "-machine" arguments 103 | if config.Accelerator == "none" { 104 | defaultArgs["-machine"] = fmt.Sprintf("type=%s", config.MachineType) 105 | s.ui.Message("WARNING: The VM will be started with no hardware acceleration.\n" + 106 | "The installation may take considerably longer to finish.\n") 107 | } else { 108 | defaultArgs["-machine"] = fmt.Sprintf("type=%s,accel=%s", 109 | config.MachineType, config.Accelerator) 110 | } 111 | 112 | // Firmware 113 | if config.Firmware != "" && !config.PFlash { 114 | defaultArgs["-bios"] = config.Firmware 115 | } 116 | 117 | // Configure "-netdev" arguments 118 | defaultArgs["-netdev"] = fmt.Sprintf("bridge,id=user.0,br=%s", config.NetBridge) 119 | if config.NetBridge == "" { 120 | defaultArgs["-netdev"] = "user,id=user.0" 121 | if config.CommConfig.Comm.Type != "none" { 122 | commHostPort := state.Get("commHostPort").(int) 123 | defaultArgs["-netdev"] = fmt.Sprintf("user,id=user.0,hostfwd=tcp::%v-:%d", commHostPort, config.CommConfig.Comm.Port()) 124 | } 125 | } 126 | 127 | // Configure "-vnc" arguments 128 | // vncPort is always set in stepConfigureVNC, so we don't need to 129 | // defensively assert 130 | vncPort := state.Get("vnc_port").(int) 131 | vncIP := config.VNCBindAddress 132 | 133 | vncRealAddress := fmt.Sprintf("%s:%d", vncIP, vncPort) 134 | vncPort = vncPort - 5900 135 | vncArgs := fmt.Sprintf("%s:%d", vncIP, vncPort) 136 | if config.VNCUsePassword { 137 | vncArgs = fmt.Sprintf("%s:%d,password", vncIP, vncPort) 138 | } 139 | defaultArgs["-vnc"] = vncArgs 140 | 141 | // Track the connection for the user 142 | vncPass, _ := state.Get("vnc_password").(string) 143 | 144 | message = getVncConnectionMessage(config.Headless, vncRealAddress, vncPass) 145 | if message != "" { 146 | s.ui.Message(message) 147 | } 148 | 149 | // Configure "-m" memory argument 150 | defaultArgs["-m"] = fmt.Sprintf("%dM", config.MemorySize) 151 | 152 | // Configure "-smp" processor hardware arguments 153 | defaultArgs["-smp"] = config.QemuSMPConfig.getDefaultCmdLine() 154 | 155 | // Configure "-cpu" cpu model argument 156 | if config.CPUModel != "" { 157 | defaultArgs["-cpu"] = config.CPUModel 158 | } 159 | 160 | // Configure "-fda" floppy disk attachment 161 | if floppyPathRaw, ok := state.GetOk("floppy_path"); ok { 162 | defaultArgs["-fda"] = floppyPathRaw.(string) 163 | } else { 164 | log.Println("Qemu Builder has no floppy files, not attaching a floppy.") 165 | } 166 | 167 | // Configure GUI display 168 | if !config.Headless { 169 | if s.atLeastVersion2 { 170 | // FIXME: "none" is a valid display option in qemu but we have 171 | // departed from the qemu usage here to instaed mean "let qemu 172 | // set a reasonable default". We need to deprecate this behavior 173 | // and let users just set "UseDefaultDisplay" if they want to let 174 | // qemu do its thing. 175 | if len(config.Display) > 0 && config.Display != "none" { 176 | defaultArgs["-display"] = config.Display 177 | } else if !config.UseDefaultDisplay { 178 | defaultArgs["-display"] = "gtk" 179 | } 180 | } else { 181 | s.ui.Message("WARNING: The version of qemu on your host doesn't support display mode.\n" + 182 | "The display parameter will be ignored.") 183 | } 184 | } 185 | 186 | if config.VTPM { 187 | vtpmSockPath := state.Get(swtpmSocketPath) 188 | defaultArgs["-chardev"] = fmt.Sprintf("socket,id=vtpm,path=%s", vtpmSockPath) 189 | defaultArgs["-tpmdev"] = "emulator,id=tpm0,chardev=vtpm" 190 | } 191 | 192 | if config.VGA != "" { 193 | defaultArgs["-vga"] = config.VGA 194 | } 195 | 196 | deviceArgs, driveArgs := s.getDeviceAndDriveArgs(config, state) 197 | defaultArgs["-device"] = deviceArgs 198 | defaultArgs["-drive"] = driveArgs 199 | 200 | return defaultArgs 201 | } 202 | 203 | func getVncConnectionMessage(headless bool, vnc string, vncPass string) string { 204 | // Configure GUI display 205 | if headless { 206 | if vnc == "" { 207 | return "The VM will be run headless, without a GUI, as configured.\n" + 208 | "If the run isn't succeeding as you expect, please enable the GUI\n" + 209 | "to inspect the progress of the build." 210 | } 211 | 212 | if vncPass != "" { 213 | return fmt.Sprintf( 214 | "The VM will be run headless, without a GUI. If you want to\n"+ 215 | "view the screen of the VM, connect via VNC to vnc://%s\n"+ 216 | "with the password: %s", vnc, vncPass) 217 | } 218 | 219 | return fmt.Sprintf( 220 | "The VM will be run headless, without a GUI. If you want to\n"+ 221 | "view the screen of the VM, connect via VNC without a password to\n"+ 222 | "vnc://%s", vnc) 223 | } 224 | return "" 225 | } 226 | 227 | func (s *stepRun) getDeviceAndDriveArgs(config *Config, state multistep.StateBag) ([]string, []string) { 228 | var deviceArgs []string 229 | var driveArgs []string 230 | availableScsiIndex := 0 231 | 232 | vmName := config.VMName 233 | imgPath := filepath.Join(config.OutputDir, vmName) 234 | 235 | // Configure virtual hard drives 236 | if s.atLeastVersion2 { 237 | drivesToAttach := []string{} 238 | 239 | if v, ok := state.GetOk("qemu_disk_paths"); ok { 240 | diskFullPaths := v.([]string) 241 | drivesToAttach = append(drivesToAttach, diskFullPaths...) 242 | } 243 | 244 | for i, drivePath := range drivesToAttach { 245 | driveArgumentString := fmt.Sprintf("file=%s,if=%s,cache=%s,discard=%s,format=%s", drivePath, config.DiskInterface, config.DiskCache, config.DiskDiscard, config.Format) 246 | if config.DiskInterface == "virtio-scsi" { 247 | // TODO: Megan: Remove this conditional. This, and the code 248 | // under the TODO below, reproduce the old behavior. While it 249 | // may be broken, the goal of this commit is to refactor in a way 250 | // that creates a result that is testably the same as the old 251 | // code. A pr will follow fixing this broken behavior. 252 | if availableScsiIndex == 0 { 253 | deviceArgs = append(deviceArgs, fmt.Sprintf("virtio-scsi-pci,id=scsi%d", 0)) 254 | } 255 | // TODO: Megan: When you remove above conditional, 256 | // set deviceArgs = append(deviceArgs, fmt.Sprintf("scsi-hd,bus=scsi%d.0,drive=drive%d", i, i)) 257 | deviceArgs = append(deviceArgs, fmt.Sprintf("scsi-hd,bus=scsi0.0,drive=drive%d", i)) 258 | driveArgumentString = fmt.Sprintf("if=none,file=%s,id=drive%d,cache=%s,discard=%s,format=%s", drivePath, i, config.DiskCache, config.DiskDiscard, config.Format) 259 | availableScsiIndex += 1 260 | } 261 | if config.DetectZeroes != "off" { 262 | driveArgumentString = fmt.Sprintf("%s,detect-zeroes=%s", driveArgumentString, config.DetectZeroes) 263 | } 264 | driveArgs = append(driveArgs, driveArgumentString) 265 | } 266 | } else { 267 | driveArgs = append(driveArgs, fmt.Sprintf("file=%s,if=%s,cache=%s,format=%s", imgPath, config.DiskInterface, config.DiskCache, config.Format)) 268 | } 269 | 270 | deviceArgs = append(deviceArgs, fmt.Sprintf("%s,netdev=user.0", config.NetDevice)) 271 | 272 | // Configure virtual CDs 273 | cdPaths := []string{} 274 | // Add the installation CD to the run command 275 | if !config.DiskImage { 276 | isoPath := state.Get("iso_path").(string) 277 | cdPaths = append(cdPaths, isoPath) 278 | } 279 | // Add our custom CD created from cd_files, if it exists 280 | cdFilesPath, ok := state.Get("cd_path").(string) 281 | if ok { 282 | if cdFilesPath != "" { 283 | cdPaths = append(cdPaths, cdFilesPath) 284 | } 285 | } 286 | for i, cdPath := range cdPaths { 287 | if config.CDROMInterface == "" { 288 | driveArgs = append(driveArgs, fmt.Sprintf("file=%s,media=cdrom", cdPath)) 289 | } else if config.CDROMInterface == "virtio-scsi" { 290 | if availableScsiIndex == 0 { 291 | deviceArgs = append(deviceArgs, fmt.Sprintf("virtio-scsi-pci,id=scsi%d", 0)) 292 | } 293 | driveArgs = append(driveArgs, fmt.Sprintf("file=%s,if=none,index=%d,id=cdrom%d,media=cdrom", cdPath, availableScsiIndex, i)) 294 | deviceArgs = append(deviceArgs, "virtio-scsi-device", fmt.Sprintf("scsi-cd,drive=cdrom%d", i)) 295 | availableScsiIndex += 1 296 | } else { 297 | driveArgs = append(driveArgs, fmt.Sprintf("file=%s,if=%s,index=%d,id=cdrom%d,media=cdrom", cdPath, config.CDROMInterface, i, i)) 298 | } 299 | } 300 | 301 | // Firmware 302 | if config.Firmware != "" && config.PFlash { 303 | driveArgs = append(driveArgs, fmt.Sprintf("file=%s,if=pflash,format=raw,readonly=on", config.Firmware)) 304 | } 305 | 306 | // EFI 307 | if config.QemuEFIBootConfig.EnableEFI { 308 | // CODE binary is loaded readonly 309 | driveArgs = append(driveArgs, fmt.Sprintf("file=%s,if=pflash,unit=0,format=raw,readonly=on", config.QemuEFIBootConfig.OVMFCode)) 310 | efivar := state.Get(efivarStateKey) 311 | // the local copy of VARS is not 312 | driveArgs = append(driveArgs, fmt.Sprintf("file=%s,if=pflash,unit=1,format=raw", efivar.(string))) 313 | } 314 | 315 | // TPM 316 | if config.VTPM { 317 | deviceArgs = append(deviceArgs, fmt.Sprintf("%s,tpmdev=tpm0", config.TPMType)) 318 | } 319 | 320 | return deviceArgs, driveArgs 321 | } 322 | 323 | func (s *stepRun) applyUserOverrides(defaultArgs map[string]interface{}, config *Config, state multistep.StateBag) ([]string, error) { 324 | // Done setting up defaults; time to process user args and defaults together 325 | // and generate output args 326 | 327 | inArgs := make(map[string][]string) 328 | if len(config.QemuArgs) > 0 { 329 | s.ui.Say("Overriding default Qemu arguments with qemuargs template option...") 330 | 331 | commHostPort := 0 332 | if config.CommConfig.Comm.Type != "none" { 333 | if v, ok := state.GetOk("commHostPort"); ok { 334 | commHostPort = v.(int) 335 | } 336 | } 337 | httpIp := state.Get("http_ip").(string) 338 | httpPort := state.Get("http_port").(int) 339 | 340 | type qemuArgsTemplateData struct { 341 | HTTPIP string 342 | HTTPPort int 343 | HTTPDir string 344 | HTTPContent map[string]string 345 | OutputDir string 346 | Name string 347 | SSHHostPort int 348 | } 349 | 350 | ictx := config.ctx 351 | ictx.Data = qemuArgsTemplateData{ 352 | HTTPIP: httpIp, 353 | HTTPPort: httpPort, 354 | HTTPDir: config.HTTPDir, 355 | HTTPContent: config.HTTPContent, 356 | OutputDir: config.OutputDir, 357 | Name: config.VMName, 358 | SSHHostPort: commHostPort, 359 | } 360 | 361 | // Interpolate each string in qemuargs 362 | newQemuArgs, err := processArgs(config.QemuArgs, &ictx) 363 | if err != nil { 364 | return nil, err 365 | } 366 | 367 | // Qemu supports multiple appearances of the same switch. This means 368 | // each key in the args hash will have an array of string values 369 | for _, qemuArgs := range newQemuArgs { 370 | key := qemuArgs[0] 371 | val := strings.Join(qemuArgs[1:], "") 372 | if _, ok := inArgs[key]; !ok { 373 | inArgs[key] = make([]string, 0) 374 | } 375 | if len(val) > 0 { 376 | inArgs[key] = append(inArgs[key], val) 377 | } 378 | } 379 | } 380 | 381 | // get any remaining missing default args from the default settings 382 | for key := range defaultArgs { 383 | if _, ok := inArgs[key]; !ok { 384 | arg := make([]string, 1) 385 | switch defaultArgs[key].(type) { 386 | case string: 387 | arg[0] = defaultArgs[key].(string) 388 | case []string: 389 | arg = defaultArgs[key].([]string) 390 | } 391 | inArgs[key] = arg 392 | } 393 | } 394 | 395 | // Check if we are missing the netDevice #6804 396 | if x, ok := inArgs["-device"]; ok { 397 | if !strings.Contains(strings.Join(x, ""), config.NetDevice) { 398 | inArgs["-device"] = append(inArgs["-device"], fmt.Sprintf("%s,netdev=user.0", config.NetDevice)) 399 | } 400 | } 401 | 402 | // Flatten to array of strings 403 | outArgs := make([]string, 0) 404 | for key, values := range inArgs { 405 | if len(values) > 0 { 406 | for idx := range values { 407 | outArgs = append(outArgs, key, values[idx]) 408 | } 409 | } else { 410 | outArgs = append(outArgs, key) 411 | } 412 | } 413 | 414 | return outArgs, nil 415 | } 416 | 417 | func (s *stepRun) getCommandArgs(config *Config, state multistep.StateBag) ([]string, error) { 418 | defaultArgs := s.getDefaultArgs(config, state) 419 | 420 | return s.applyUserOverrides(defaultArgs, config, state) 421 | } 422 | 423 | func processArgs(args [][]string, ctx *interpolate.Context) ([][]string, error) { 424 | var err error 425 | 426 | if args == nil { 427 | return make([][]string, 0), err 428 | } 429 | 430 | newArgs := make([][]string, len(args)) 431 | for argsIdx, rowArgs := range args { 432 | parms := make([]string, len(rowArgs)) 433 | newArgs[argsIdx] = parms 434 | for i, parm := range rowArgs { 435 | parms[i], err = interpolate.Render(parm, ctx) 436 | if err != nil { 437 | return nil, err 438 | } 439 | } 440 | } 441 | 442 | return newArgs, err 443 | } 444 | -------------------------------------------------------------------------------- /builder/qemu/step_set_iso.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package qemu 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "net/http" 10 | 11 | "github.com/hashicorp/packer-plugin-sdk/multistep" 12 | "github.com/hashicorp/packer-plugin-sdk/net" 13 | packersdk "github.com/hashicorp/packer-plugin-sdk/packer" 14 | ) 15 | 16 | // This step set iso_patch to available url 17 | type stepSetISO struct { 18 | ResultKey string 19 | Url []string 20 | } 21 | 22 | func (s *stepSetISO) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { 23 | ui := state.Get("ui").(packersdk.Ui) 24 | 25 | iso_path := "" 26 | 27 | for _, url := range s.Url { 28 | req, err := http.NewRequest("HEAD", url, nil) 29 | if err != nil { 30 | continue 31 | } 32 | 33 | req.Header.Set("User-Agent", "Packer") 34 | 35 | httpClient := net.HttpClientWithEnvironmentProxy() 36 | 37 | res, err := httpClient.Do(req) 38 | if err == nil && (res.StatusCode >= 200 && res.StatusCode < 300) { 39 | if res.Header.Get("Accept-Ranges") == "bytes" { 40 | iso_path = url 41 | } 42 | } 43 | } 44 | 45 | if iso_path == "" { 46 | err := fmt.Errorf("No byte serving support. The HTTP server must support Accept-Ranges=bytes") 47 | state.Put("error", err) 48 | ui.Error(err.Error()) 49 | return multistep.ActionHalt 50 | } 51 | 52 | state.Put(s.ResultKey, iso_path) 53 | 54 | return multistep.ActionContinue 55 | } 56 | 57 | func (s *stepSetISO) Cleanup(state multistep.StateBag) {} 58 | -------------------------------------------------------------------------------- /builder/qemu/step_shutdown.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package qemu 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | "fmt" 10 | "log" 11 | "time" 12 | 13 | "github.com/hashicorp/packer-plugin-sdk/communicator" 14 | "github.com/hashicorp/packer-plugin-sdk/multistep" 15 | packersdk "github.com/hashicorp/packer-plugin-sdk/packer" 16 | ) 17 | 18 | // This step shuts down the machine. It first attempts to do so gracefully, 19 | // but ultimately forcefully shuts it down if that fails. 20 | // 21 | // Uses: 22 | // 23 | // communicator packersdk.Communicator 24 | // config *config 25 | // driver Driver 26 | // ui packersdk.Ui 27 | // 28 | // Produces: 29 | // 30 | // 31 | type stepShutdown struct { 32 | ShutdownCommand string 33 | ShutdownTimeout time.Duration 34 | Comm *communicator.Config 35 | } 36 | 37 | func (s *stepShutdown) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { 38 | driver := state.Get("driver").(Driver) 39 | ui := state.Get("ui").(packersdk.Ui) 40 | 41 | if s.ShutdownCommand != "" && s.Comm.Type != "none" { 42 | comm := state.Get("communicator").(packersdk.Communicator) 43 | ui.Say("Gracefully halting virtual machine...") 44 | log.Printf("Executing shutdown command: %s", s.ShutdownCommand) 45 | cmd := &packersdk.RemoteCmd{Command: s.ShutdownCommand} 46 | if err := cmd.RunWithUi(ctx, comm, ui); err != nil { 47 | err := fmt.Errorf("Failed to send shutdown command: %s", err) 48 | state.Put("error", err) 49 | ui.Error(err.Error()) 50 | return multistep.ActionHalt 51 | } 52 | 53 | // Start the goroutine that will time out our graceful attempt 54 | cancelCh := make(chan struct{}, 1) 55 | go func() { 56 | defer close(cancelCh) 57 | <-time.After(s.ShutdownTimeout) 58 | }() 59 | 60 | log.Printf("Waiting max %s for shutdown to complete", s.ShutdownTimeout) 61 | if ok := driver.WaitForShutdown(cancelCh); !ok { 62 | err := errors.New("Timeout while waiting for machine to shut down.") 63 | state.Put("error", err) 64 | ui.Error(err.Error()) 65 | return multistep.ActionHalt 66 | } 67 | } else { 68 | ui.Say("Halting the virtual machine...") 69 | if err := driver.Stop(); err != nil { 70 | err := fmt.Errorf("Error stopping VM: %s", err) 71 | state.Put("error", err) 72 | ui.Error(err.Error()) 73 | return multistep.ActionHalt 74 | } 75 | } 76 | 77 | log.Println("VM shut down.") 78 | return multistep.ActionContinue 79 | } 80 | 81 | func (s *stepShutdown) Cleanup(state multistep.StateBag) {} 82 | -------------------------------------------------------------------------------- /builder/qemu/step_shutdown_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package qemu 5 | 6 | import ( 7 | "context" 8 | "testing" 9 | "time" 10 | 11 | "github.com/hashicorp/packer-plugin-sdk/communicator" 12 | "github.com/hashicorp/packer-plugin-sdk/multistep" 13 | packersdk "github.com/hashicorp/packer-plugin-sdk/packer" 14 | ) 15 | 16 | func Test_Shutdown_Null_success(t *testing.T) { 17 | state := new(multistep.BasicStateBag) 18 | state.Put("ui", packersdk.TestUi(t)) 19 | driverMock := new(DriverMock) 20 | driverMock.WaitForShutdownState = true 21 | state.Put("driver", driverMock) 22 | 23 | step := &stepShutdown{ 24 | ShutdownCommand: "", 25 | ShutdownTimeout: 5 * time.Minute, 26 | Comm: &communicator.Config{ 27 | Type: "none", 28 | }, 29 | } 30 | action := step.Run(context.TODO(), state) 31 | if action != multistep.ActionContinue { 32 | t.Fatalf("Should have successfully shut down.") 33 | } 34 | err := state.Get("error") 35 | if err != nil { 36 | err = err.(error) 37 | t.Fatalf("Shutdown shouldn't have errored; err: %v", err) 38 | } 39 | } 40 | 41 | func Test_Shutdown_Null_failure(t *testing.T) { 42 | state := new(multistep.BasicStateBag) 43 | state.Put("ui", packersdk.TestUi(t)) 44 | driverMock := new(DriverMock) 45 | state.Put("driver", driverMock) 46 | 47 | step := &stepShutdown{ 48 | ShutdownCommand: "", 49 | ShutdownTimeout: 5 * time.Minute, 50 | Comm: &communicator.Config{ 51 | Type: "none", 52 | }, 53 | } 54 | action := step.Run(context.TODO(), state) 55 | if action != multistep.ActionContinue { 56 | t.Fatalf("Should have successfully shut down.") 57 | } 58 | 59 | err := state.Get("error") 60 | if err != nil { 61 | err = err.(error) 62 | t.Fatalf("Shutdown shouldn't have errored; err: %v", err) 63 | } 64 | } 65 | 66 | func Test_Shutdown_NoShutdownCommand(t *testing.T) { 67 | state := new(multistep.BasicStateBag) 68 | state.Put("ui", packersdk.TestUi(t)) 69 | driverMock := new(DriverMock) 70 | state.Put("driver", driverMock) 71 | 72 | step := &stepShutdown{ 73 | ShutdownCommand: "", 74 | ShutdownTimeout: 5 * time.Minute, 75 | Comm: &communicator.Config{ 76 | Type: "ssh", 77 | }, 78 | } 79 | action := step.Run(context.TODO(), state) 80 | if action != multistep.ActionContinue { 81 | t.Fatalf("Should have successfully shut down.") 82 | } 83 | 84 | if !driverMock.StopCalled { 85 | t.Fatalf("should have called Stop through the driver.") 86 | } 87 | err := state.Get("error") 88 | if err != nil { 89 | err = err.(error) 90 | t.Fatalf("Shutdown shouldn't have errored; err: %v", err) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /builder/qemu/step_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package qemu 5 | 6 | import ( 7 | "bytes" 8 | "testing" 9 | 10 | "github.com/hashicorp/packer-plugin-sdk/multistep" 11 | packersdk "github.com/hashicorp/packer-plugin-sdk/packer" 12 | ) 13 | 14 | func testState(t *testing.T) multistep.StateBag { 15 | state := new(multistep.BasicStateBag) 16 | state.Put("driver", new(DriverMock)) 17 | state.Put("ui", &packersdk.BasicUi{ 18 | Reader: new(bytes.Buffer), 19 | Writer: new(bytes.Buffer), 20 | }) 21 | return state 22 | } 23 | -------------------------------------------------------------------------------- /builder/qemu/step_type_boot_command.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package qemu 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "log" 10 | "net" 11 | "time" 12 | 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 | "github.com/hashicorp/packer-plugin-sdk/template/interpolate" 17 | "github.com/mitchellh/go-vnc" 18 | ) 19 | 20 | const KeyLeftShift uint32 = 0xFFE1 21 | 22 | type bootCommandTemplateData struct { 23 | HTTPIP string 24 | HTTPPort int 25 | Name string 26 | SSHPublicKey string 27 | } 28 | 29 | // This step "types" the boot command into the VM over VNC. 30 | // 31 | // Uses: 32 | // 33 | // config *config 34 | // http_port int 35 | // ui packersdk.Ui 36 | // vnc_port int 37 | // 38 | // Produces: 39 | // 40 | // 41 | type stepTypeBootCommand struct{} 42 | 43 | func (s *stepTypeBootCommand) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { 44 | config := state.Get("config").(*Config) 45 | command := config.VNCConfig.FlatBootCommand() 46 | bootSteps := config.BootSteps 47 | 48 | if len(command) > 0 { 49 | bootSteps = [][]string{{command}} 50 | } 51 | 52 | return typeBootCommands(ctx, state, bootSteps) 53 | } 54 | 55 | func (*stepTypeBootCommand) Cleanup(multistep.StateBag) {} 56 | 57 | func typeBootCommands(ctx context.Context, state multistep.StateBag, bootSteps [][]string) multistep.StepAction { 58 | config := state.Get("config").(*Config) 59 | debug := state.Get("debug").(bool) 60 | httpPort := state.Get("http_port").(int) 61 | ui := state.Get("ui").(packersdk.Ui) 62 | vncPort := state.Get("vnc_port").(int) 63 | vncIP := config.VNCBindAddress 64 | vncPassword := state.Get("vnc_password") 65 | 66 | if config.VNCConfig.DisableVNC { 67 | log.Println("Skipping boot command step...") 68 | return multistep.ActionContinue 69 | } 70 | 71 | // Wait the for the vm to boot. 72 | if int64(config.BootWait) > 0 { 73 | ui.Say(fmt.Sprintf("Waiting %s for boot...", config.BootWait)) 74 | select { 75 | case <-time.After(config.BootWait): 76 | break 77 | case <-ctx.Done(): 78 | return multistep.ActionHalt 79 | } 80 | } 81 | 82 | var pauseFn multistep.DebugPauseFn 83 | if debug { 84 | pauseFn = state.Get("pauseFn").(multistep.DebugPauseFn) 85 | } 86 | 87 | // Connect to VNC 88 | ui.Say(fmt.Sprintf("Connecting to VM via VNC (%s:%d)", vncIP, vncPort)) 89 | 90 | nc, err := net.Dial("tcp", fmt.Sprintf("%s:%d", vncIP, vncPort)) 91 | if err != nil { 92 | err := fmt.Errorf("Error connecting to VNC: %s", err) 93 | state.Put("error", err) 94 | ui.Error(err.Error()) 95 | return multistep.ActionHalt 96 | } 97 | defer nc.Close() 98 | 99 | var auth []vnc.ClientAuth 100 | 101 | if vncPassword != nil && len(vncPassword.(string)) > 0 { 102 | auth = []vnc.ClientAuth{&vnc.PasswordAuth{Password: vncPassword.(string)}} 103 | } else { 104 | auth = []vnc.ClientAuth{new(vnc.ClientAuthNone)} 105 | } 106 | 107 | c, err := vnc.Client(nc, &vnc.ClientConfig{Auth: auth, Exclusive: false}) 108 | if err != nil { 109 | err := fmt.Errorf("Error handshaking with VNC: %s", err) 110 | state.Put("error", err) 111 | ui.Error(err.Error()) 112 | return multistep.ActionHalt 113 | } 114 | defer c.Close() 115 | 116 | log.Printf("Connected to VNC desktop: %s", c.DesktopName) 117 | 118 | hostIP := state.Get("http_ip").(string) 119 | SSHPublicKey := string(config.CommConfig.Comm.SSHPublicKey) 120 | configCtx := config.ctx 121 | configCtx.Data = &bootCommandTemplateData{ 122 | hostIP, 123 | httpPort, 124 | config.VMName, 125 | SSHPublicKey, 126 | } 127 | 128 | d := bootcommand.NewVNCDriver(c, config.VNCConfig.BootKeyInterval) 129 | 130 | ui.Say("Typing the boot commands over VNC...") 131 | 132 | for _, step := range bootSteps { 133 | if len(step) == 0 { 134 | continue 135 | } 136 | 137 | var description string 138 | 139 | if len(step) >= 2 { 140 | description = step[1] 141 | } else { 142 | description = "" 143 | } 144 | 145 | if len(description) > 0 { 146 | ui.Say(fmt.Sprintf("Typing boot command for: %s", description)) 147 | } 148 | 149 | command, err := interpolate.Render(step[0], &configCtx) 150 | 151 | if err != nil { 152 | err := fmt.Errorf("Error preparing boot command: %s", err) 153 | state.Put("error", err) 154 | ui.Error(err.Error()) 155 | return multistep.ActionHalt 156 | } 157 | 158 | seq, err := bootcommand.GenerateExpressionSequence(command) 159 | if err != nil { 160 | err := fmt.Errorf("Error generating boot command: %s", err) 161 | state.Put("error", err) 162 | ui.Error(err.Error()) 163 | return multistep.ActionHalt 164 | } 165 | 166 | if err := seq.Do(ctx, d); err != nil { 167 | err := fmt.Errorf("Error running boot command: %s", err) 168 | state.Put("error", err) 169 | ui.Error(err.Error()) 170 | return multistep.ActionHalt 171 | } 172 | 173 | if pauseFn != nil { 174 | var message string 175 | 176 | if len(description) > 0 { 177 | message = fmt.Sprintf("boot description: \"%s\", command: %s", description, command) 178 | } else { 179 | message = fmt.Sprintf("boot_command: %s", command) 180 | } 181 | 182 | pauseFn(multistep.DebugLocationAfterRun, message, state) 183 | } 184 | } 185 | 186 | return multistep.ActionContinue 187 | } 188 | -------------------------------------------------------------------------------- /builder/qemu/step_wait_guest_address.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package qemu 5 | 6 | import ( 7 | "bufio" 8 | "context" 9 | "fmt" 10 | "log" 11 | "os" 12 | "strconv" 13 | "strings" 14 | "time" 15 | 16 | "github.com/hashicorp/packer-plugin-sdk/multistep" 17 | packersdk "github.com/hashicorp/packer-plugin-sdk/packer" 18 | 19 | "github.com/digitalocean/go-qemu/qmp" 20 | ) 21 | 22 | // This step waits for the guest address to become available in the network 23 | // bridge, then it sets the guestAddress state property. 24 | type stepWaitGuestAddress struct { 25 | CommunicatorType string 26 | NetBridge string 27 | 28 | timeout time.Duration 29 | } 30 | 31 | func (s *stepWaitGuestAddress) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { 32 | ui := state.Get("ui").(packersdk.Ui) 33 | 34 | if s.CommunicatorType == "none" { 35 | ui.Message("No communicator is configured -- skipping StepWaitGuestAddress") 36 | return multistep.ActionContinue 37 | } 38 | if s.NetBridge == "" { 39 | ui.Message("Not using a NetBridge -- skipping StepWaitGuestAddress") 40 | return multistep.ActionContinue 41 | } 42 | 43 | qmpMonitor := state.Get("qmp_monitor").(*qmp.SocketMonitor) 44 | ctx, cancel := context.WithTimeout(ctx, s.timeout) 45 | defer cancel() 46 | 47 | ui.Say(fmt.Sprintf("Waiting for the guest address to become available in the %s network bridge...", s.NetBridge)) 48 | for { 49 | guestAddress := getGuestAddress(qmpMonitor, s.NetBridge, "user.0") 50 | if guestAddress != "" { 51 | log.Printf("Found guest address %s", guestAddress) 52 | state.Put("guestAddress", guestAddress) 53 | return multistep.ActionContinue 54 | } 55 | select { 56 | case <-time.After(10 * time.Second): 57 | continue 58 | case <-ctx.Done(): 59 | return multistep.ActionHalt 60 | } 61 | } 62 | } 63 | 64 | func (s *stepWaitGuestAddress) Cleanup(state multistep.StateBag) { 65 | } 66 | 67 | func getGuestAddress(qmpMonitor *qmp.SocketMonitor, bridgeName string, deviceName string) string { 68 | devices, err := getNetDevices(qmpMonitor) 69 | if err != nil { 70 | log.Printf("Could not retrieve QEMU QMP network device list: %v", err) 71 | return "" 72 | } 73 | 74 | for _, device := range devices { 75 | if device.Name == deviceName { 76 | ipAddress, _ := getDeviceIPAddress(bridgeName, device.MacAddress) 77 | return ipAddress 78 | } 79 | } 80 | 81 | log.Printf("QEMU QMP network device %s was not found", deviceName) 82 | return "" 83 | } 84 | 85 | func getDeviceIPAddress(device string, macAddress string) (string, error) { 86 | // this parses /proc/net/arp to retrieve the given device IP address. 87 | // 88 | // /proc/net/arp is normally someting alike: 89 | // 90 | // IP address HW type Flags HW address Mask Device 91 | // 192.168.121.111 0x1 0x2 52:54:00:12:34:56 * virbr0 92 | // 93 | 94 | const ( 95 | IPAddressIndex int = iota 96 | HWTypeIndex 97 | FlagsIndex 98 | HWAddressIndex 99 | MaskIndex 100 | DeviceIndex 101 | ) 102 | 103 | // see ARP flags at https://github.com/torvalds/linux/blob/v5.4/include/uapi/linux/if_arp.h#L132 104 | const ( 105 | AtfCom int = 0x02 // ATF_COM (complete) 106 | ) 107 | 108 | f, err := os.Open("/proc/net/arp") 109 | if err != nil { 110 | return "", fmt.Errorf("failed to open /proc/net/arp: %w", err) 111 | } 112 | defer f.Close() 113 | 114 | s := bufio.NewScanner(f) 115 | s.Scan() 116 | 117 | for s.Scan() { 118 | fields := strings.Fields(s.Text()) 119 | 120 | if device != "" && fields[DeviceIndex] != device { 121 | continue 122 | } 123 | 124 | if fields[HWAddressIndex] != macAddress { 125 | continue 126 | } 127 | 128 | flags, err := strconv.ParseInt(fields[FlagsIndex], 0, 32) 129 | if err != nil { 130 | return "", fmt.Errorf("failed to parse /proc/net/arp flags field %s: %w", fields[FlagsIndex], err) 131 | } 132 | 133 | if int(flags)&AtfCom == AtfCom { 134 | return fields[IPAddressIndex], nil 135 | } 136 | } 137 | 138 | return "", fmt.Errorf("could not find %s", macAddress) 139 | } 140 | -------------------------------------------------------------------------------- /builder/qemu/testdata/floppies/bar.bat: -------------------------------------------------------------------------------- 1 | Echo I am a floppy with a batch file -------------------------------------------------------------------------------- /builder/qemu/testdata/floppies/foo.ps1: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | Write-Host "I am a floppy with some Powershell" -------------------------------------------------------------------------------- /docs-partials/builder/qemu/CommConfig-not-required.mdx: -------------------------------------------------------------------------------- 1 | 2 | 3 | - `host_port_min` (int) - The minimum port to use for the Communicator port on the host machine which is forwarded 4 | to the SSH or WinRM port on the guest machine. By default this is 2222. 5 | 6 | - `host_port_max` (int) - The maximum port to use for the Communicator port on the host machine which is forwarded 7 | to the SSH or WinRM port on the guest machine. Because Packer often runs in parallel, 8 | Packer will choose a randomly available port in this range to use as the 9 | host port. By default this is 4444. 10 | 11 | - `skip_nat_mapping` (bool) - Defaults to false. When enabled, Packer 12 | does not setup forwarded port mapping for communicator (SSH or WinRM) requests and uses ssh_port or winrm_port 13 | on the host to communicate to the virtual machine. 14 | 15 | 16 | -------------------------------------------------------------------------------- /docs-partials/builder/qemu/QemuEFIBootConfig-not-required.mdx: -------------------------------------------------------------------------------- 1 | 2 | 3 | - `efi_boot` (bool) - Boot in EFI mode instead of BIOS. This is required for more modern 4 | guest OS. If either or both of `efi_firmware_code` or 5 | `efi_firmware_vars` are defined, this will implicitely be set to `true`. 6 | 7 | NOTE: when using a Secure-Boot enabled firmware, the machine type has 8 | to be q35, otherwise qemu will not boot. 9 | 10 | - `efi_firmware_code` (string) - Path to the CODE part of OVMF (or other compatible firmwares) 11 | The OVMF_CODE.fd file contains the bootstrap code for booting in EFI 12 | mode, and requires a separate VARS.fd file to be able to persist data 13 | between boot cycles. 14 | 15 | Default: `/usr/share/OVMF/OVMF_CODE.fd` 16 | 17 | - `efi_firmware_vars` (string) - Path to the VARS corresponding to the OVMF code file. 18 | 19 | Default: `/usr/share/OVMF/OVMF_VARS.fd` 20 | 21 | - `efi_drop_efivars` (bool) - Drop the efivars.fd file in the exported artifact. 22 | 23 | In addition to the disks created by the builder, we also expose the 24 | `efivars.fd` file if the image was booted with UEFI enabled. 25 | 26 | However, if the output is consumed by a post-processor (like AWS, 27 | GCP, etc.), this may not be supported by the code, and since the file 28 | is not a disk image, this will error. 29 | This option can then be used to remove the `efivars.fd` from the 30 | artifact produced by the builder, so it only lists the disks produced 31 | instead. 32 | 33 | 34 | -------------------------------------------------------------------------------- /docs-partials/builder/qemu/QemuEFIBootConfig.mdx: -------------------------------------------------------------------------------- 1 | 2 | 3 | Booting in EFI mode 4 | 5 | Use these options if wanting to boot on a UEFI firmware, as the options to 6 | do so are different from what BIOS (default) booting will require. 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs-partials/builder/qemu/QemuImgArgs-not-required.mdx: -------------------------------------------------------------------------------- 1 | 2 | 3 | - `convert` ([]string) - Convert 4 | 5 | - `create` ([]string) - Create 6 | 7 | - `resize` ([]string) - Resize 8 | 9 | 10 | -------------------------------------------------------------------------------- /docs-partials/builder/qemu/QemuSMPConfig-not-required.mdx: -------------------------------------------------------------------------------- 1 | 2 | 3 | - `cpus` (int) - The number of virtual cpus to use when building the VM. 4 | 5 | If undefined, the value will either be `1`, or the product of 6 | `sockets * cores * threads` 7 | 8 | If this is defined in conjunction with any topology specifier (sockets, 9 | cores and/or threads), the smallest of the two will be used. 10 | 11 | If the cpu count is the only thing specified, qemu's default behaviour 12 | regarding topology will be applied. 13 | The behaviour depends on the version of qemu; before version 6.2, sockets 14 | were preferred to cores, from version 6.2 onwards, cores are preferred. 15 | 16 | - `sockets` (int) - The number of sockets to use when building the VM. 17 | The default is `1` socket. 18 | The socket count must not be higher than the CPU count. 19 | 20 | - `cores` (int) - The number of cores per CPU to use when building the VM. 21 | The default is `1` core per CPU. 22 | 23 | - `threads` (int) - The number of threads per core to use when building the VM. 24 | The default is `1` thread per core. 25 | 26 | 27 | -------------------------------------------------------------------------------- /docs-partials/builder/qemu/QemuSMPConfig.mdx: -------------------------------------------------------------------------------- 1 | 2 | 3 | QemuSMPConfig sets the smp configuration option for the Qemu command-line 4 | 5 | The smp option sets the number of vCPUs to expose to the VM, the final 6 | number of available vCPUs is `sockets * cores * threads`. 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | 2 | The Qemu Packer Plugin comes with a single builder able to create KVM virtual machine images. 3 | 4 | 5 | ### Installation 6 | 7 | To install this plugin add this code into your Packer configuration and run [packer init](/packer/docs/commands/init) 8 | 9 | ```hcl 10 | packer { 11 | required_plugins { 12 | qemu = { 13 | version = "~> 1" 14 | source = "github.com/hashicorp/qemu" 15 | } 16 | } 17 | } 18 | ``` 19 | Alternatively, you can use `packer plugins install` to manage installation of this plugin. 20 | 21 | ```sh 22 | packer plugins install github.com/hashicorp/qemu 23 | ``` 24 | 25 | ### Components 26 | 27 | #### Builders 28 | 29 | - [qemu](/packer/integrations/hashicorp/qemu/latest/components/builder/qemu) - The QEMU builder is able to create [KVM](http://www.linux-kvm.org) virtual machine images. 30 | 31 | -------------------------------------------------------------------------------- /docs/builders/qemu.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | modeline: | 3 | vim: set ft=pandoc: 4 | description: | 5 | The Qemu Packer builder is able to create KVM virtual machine images. 6 | page_title: QEMU - Builders 7 | nav_title: QEMU 8 | --- 9 | 10 | # QEMU Builder 11 | 12 | Type: `qemu` 13 | Artifact BuilderId: `transcend.qemu` 14 | 15 | The Qemu Packer builder is able to create [KVM](http://www.linux-kvm.org) virtual 16 | machine images. 17 | 18 | The builder builds a virtual machine by creating a new virtual machine from 19 | scratch, booting it, installing an OS, rebooting the machine with the boot media 20 | as the virtual hard drive, provisioning software within the OS, then shutting it 21 | down. The result of the Qemu builder is a directory containing the image file 22 | necessary to run the virtual machine on KVM. 23 | 24 | ## Basic Example 25 | 26 | Here is a basic example. This example is functional so long as you fixup paths 27 | to files, URLS for ISOs and checksums. 28 | 29 | **HCL2** 30 | 31 | ```hcl 32 | source "qemu" "example" { 33 | iso_url = "http://mirror.raystedman.net/centos/6/isos/x86_64/CentOS-6.9-x86_64-minimal.iso" 34 | iso_checksum = "md5:af4a1640c0c6f348c6c41f1ea9e192a2" 35 | output_directory = "output_centos_tdhtest" 36 | shutdown_command = "echo 'packer' | sudo -S shutdown -P now" 37 | disk_size = "5000M" 38 | format = "qcow2" 39 | accelerator = "kvm" 40 | http_directory = "path/to/httpdir" 41 | ssh_username = "root" 42 | ssh_password = "s0m3password" 43 | ssh_timeout = "20m" 44 | vm_name = "tdhtest" 45 | net_device = "virtio-net" 46 | disk_interface = "virtio" 47 | boot_wait = "10s" 48 | boot_command = [" text ks=http://{{ .HTTPIP }}:{{ .HTTPPort }}/centos6-ks.cfg"] 49 | } 50 | 51 | build { 52 | sources = ["source.qemu.example"] 53 | } 54 | ``` 55 | 56 | **JSON** 57 | 58 | ```json 59 | { 60 | "builders": [ 61 | { 62 | "type": "qemu", 63 | "iso_url": "http://mirror.raystedman.net/centos/6/isos/x86_64/CentOS-6.9-x86_64-minimal.iso", 64 | "iso_checksum": "md5:af4a1640c0c6f348c6c41f1ea9e192a2", 65 | "output_directory": "output_centos_tdhtest", 66 | "shutdown_command": "echo 'packer' | sudo -S shutdown -P now", 67 | "disk_size": "5000M", 68 | "format": "qcow2", 69 | "accelerator": "kvm", 70 | "http_directory": "path/to/httpdir", 71 | "ssh_username": "root", 72 | "ssh_password": "s0m3password", 73 | "ssh_timeout": "20m", 74 | "vm_name": "tdhtest", 75 | "net_device": "virtio-net", 76 | "disk_interface": "virtio", 77 | "boot_wait": "10s", 78 | "boot_command": [ 79 | " text ks=http://{{ .HTTPIP }}:{{ .HTTPPort }}/centos6-ks.cfg" 80 | ] 81 | } 82 | ] 83 | } 84 | ``` 85 | 86 | This is an example only, and will time out waiting for SSH because we have not 87 | provided a kickstart file. You must add a valid kickstart file to the 88 | "http_directory" and then provide the file in the "boot_command" in order for 89 | this build to run. We recommend you check out the 90 | [Community Templates](https://developer.hashicorp.com/packer/docs/community-tools#templates) 91 | for a practical usage example. 92 | 93 | Note that you will need to set `"headless": true` if you are running Packer 94 | on a Linux server without X11; or if you are connected via SSH to a remote 95 | Linux server and have not enabled X11 forwarding (`ssh -X`). 96 | 97 | ## Qemu Specific Configuration Reference 98 | 99 | There are many configuration options available for the builder. In addition to 100 | the items listed here, you will want to look at the general configuration 101 | references for [ISO](#iso-configuration), 102 | [HTTP](#http-directory-configuration), 103 | [Floppy](#floppy-configuration), 104 | [Boot](#boot-configuration), 105 | [Shutdown](#shutdown-configuration), 106 | [Communicator](#communicator-configuration) 107 | configuration references, which are 108 | necessary for this build to succeed and can be found further down the page. 109 | 110 | ### Optional: 111 | 112 | @include 'builder/qemu/Config-not-required.mdx' 113 | 114 | ## ISO Configuration 115 | 116 | @include 'packer-plugin-sdk/multistep/commonsteps/ISOConfig.mdx' 117 | 118 | ### Required: 119 | 120 | @include 'packer-plugin-sdk/multistep/commonsteps/ISOConfig-required.mdx' 121 | 122 | ### Optional: 123 | 124 | @include 'packer-plugin-sdk/multistep/commonsteps/ISOConfig-not-required.mdx' 125 | 126 | ## Http directory configuration 127 | 128 | @include 'packer-plugin-sdk/multistep/commonsteps/HTTPConfig.mdx' 129 | 130 | ### Optional: 131 | 132 | @include 'packer-plugin-sdk/multistep/commonsteps/HTTPConfig-not-required.mdx' 133 | 134 | ## Floppy configuration 135 | 136 | @include 'packer-plugin-sdk/multistep/commonsteps/FloppyConfig.mdx' 137 | 138 | ### Optional: 139 | 140 | @include 'packer-plugin-sdk/multistep/commonsteps/FloppyConfig-not-required.mdx' 141 | 142 | ### CD configuration 143 | 144 | @include 'packer-plugin-sdk/multistep/commonsteps/CDConfig.mdx' 145 | 146 | #### Optional: 147 | 148 | @include 'packer-plugin-sdk/multistep/commonsteps/CDConfig-not-required.mdx' 149 | 150 | ## Shutdown configuration 151 | 152 | ### Optional: 153 | 154 | @include 'packer-plugin-sdk/shutdowncommand/ShutdownConfig-not-required.mdx' 155 | 156 | ## Communicator configuration 157 | 158 | ### Optional common fields: 159 | 160 | @include 'packer-plugin-sdk/communicator/Config-not-required.mdx' 161 | 162 | @include 'builder/qemu/CommConfig-not-required.mdx' 163 | 164 | ### Optional SSH fields: 165 | 166 | @include 'packer-plugin-sdk/communicator/SSH-not-required.mdx' 167 | 168 | @include 'packer-plugin-sdk/communicator/SSH-Private-Key-File-not-required.mdx' 169 | 170 | @include 'packer-plugin-sdk/communicator/SSHTemporaryKeyPair-not-required.mdx' 171 | 172 | ### Optional WinRM fields: 173 | 174 | @include 'packer-plugin-sdk/communicator/WinRM-not-required.mdx' 175 | 176 | ## Boot Configuration 177 | 178 | @include 'packer-plugin-sdk/bootcommand/VNCConfig.mdx' 179 | 180 | @include 'packer-plugin-sdk/bootcommand/BootConfig.mdx' 181 | 182 | ### Optional: 183 | 184 | @include 'packer-plugin-sdk/bootcommand/VNCConfig-not-required.mdx' 185 | 186 | @include 'packer-plugin-sdk/bootcommand/BootConfig-not-required.mdx' 187 | 188 | ## EFI Boot Configuration 189 | 190 | @include 'builder/qemu/QemuEFIBootConfig.mdx' 191 | 192 | ### Optional 193 | 194 | @include 'builder/qemu/QemuEFIBootConfig-not-required.mdx' 195 | 196 | ## SMP Configuration 197 | 198 | @include 'builder/qemu/QemuSMPConfig.mdx' 199 | 200 | ### Optional 201 | 202 | @include 'builder/qemu/QemuSMPConfig-not-required.mdx' 203 | 204 | ### Communicator Configuration 205 | 206 | #### Optional: 207 | 208 | @include 'packer-plugin-sdk/communicator/Config-not-required.mdx' 209 | 210 | ### SSH key pair automation 211 | 212 | The QEMU builder can inject the current SSH key pair's public key into 213 | the template using the `SSHPublicKey` template engine. This is the SSH public 214 | key as a line in OpenSSH authorized_keys format. 215 | 216 | When a private key is provided using `ssh_private_key_file`, the key's 217 | corresponding public key can be accessed using the above engine. 218 | 219 | @include 'packer-plugin-sdk/communicator/SSH-Private-Key-File-not-required.mdx' 220 | 221 | If `ssh_password` and `ssh_private_key_file` are not specified, Packer will 222 | automatically generate en ephemeral key pair. The key pair's public key can 223 | be accessed using the template engine. 224 | 225 | For example, the public key can be provided in the boot command as a URL 226 | encoded string by appending `| urlquery` to the variable: 227 | 228 | In JSON: 229 | 230 | ```json 231 | "boot_command": [ 232 | " text ks=http://{{ .HTTPIP }}:{{ .HTTPPort }}/ks.cfg PACKER_USER={{ user `username` }} PACKER_AUTHORIZED_KEY={{ .SSHPublicKey | urlquery }}" 233 | ] 234 | ``` 235 | 236 | In HCL2: 237 | 238 | ```hcl 239 | boot_command = [ 240 | " text ks=http://{{ .HTTPIP }}:{{ .HTTPPort }}/ks.cfg PACKER_USER={{ user `username` }} PACKER_AUTHORIZED_KEY={{ .SSHPublicKey | urlquery }}" 241 | ] 242 | ``` 243 | 244 | A kickstart could then leverage those fields from the kernel command line by 245 | decoding the URL-encoded public key: 246 | 247 | ```shell 248 | %post 249 | 250 | # Newly created users need the file/folder framework for SSH key authentication. 251 | umask 0077 252 | mkdir /etc/skel/.ssh 253 | touch /etc/skel/.ssh/authorized_keys 254 | 255 | # Loop over the command line. Set interesting variables. 256 | for x in $(cat /proc/cmdline) 257 | do 258 | case $x in 259 | PACKER_USER=*) 260 | PACKER_USER="${x#*=}" 261 | ;; 262 | PACKER_AUTHORIZED_KEY=*) 263 | # URL decode $encoded into $PACKER_AUTHORIZED_KEY 264 | encoded=$(echo "${x#*=}" | tr '+' ' ') 265 | printf -v PACKER_AUTHORIZED_KEY '%b' "${encoded//%/\\x}" 266 | ;; 267 | esac 268 | done 269 | 270 | # Create/configure packer user, if any. 271 | if [ -n "$PACKER_USER" ] 272 | then 273 | useradd $PACKER_USER 274 | echo "%$PACKER_USER ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers.d/$PACKER_USER 275 | [ -n "$PACKER_AUTHORIZED_KEY" ] && echo $PACKER_AUTHORIZED_KEY >> $(eval echo ~"$PACKER_USER")/.ssh/authorized_keys 276 | fi 277 | 278 | %end 279 | ``` 280 | 281 | ### Troubleshooting 282 | 283 | #### Invalid Keymaps 284 | 285 | Some users have experienced errors complaining about invalid keymaps. This 286 | seems to be related to having a `common` directory or file in the directory 287 | they've run Packer in, like the Packer source directory. This appears to be an 288 | upstream bug with qemu, and the best solution for now is to remove the 289 | file/directory or run in another directory. 290 | 291 | Some users have reported issues with incorrect keymaps using qemu version 2.11. 292 | This is a bug with qemu, and the solution is to upgrade, or downgrade to 2.10.1 293 | or earlier. 294 | 295 | #### Corrupted image after Packer calls qemu-img convert on OSX 296 | 297 | Due to an upstream bug with `qemu-img convert` on OSX, sometimes the qemu-img 298 | convert call will create a corrupted image. If this is an issue for you, make 299 | sure that the the output format (provided using the option `format`) matches 300 | the input file's format and file extension, and Packer will 301 | perform a simple copy operation instead. You will also want to set 302 | `"skip_compaction": true,` and `"disk_compression": false` to skip a final 303 | image conversion at the end of the build. See 304 | https://bugs.launchpad.net/qemu/+bug/1776920 for more details. 305 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | ## The Example Folder 2 | 3 | This folder must contain a fully working example of the plugin usage. The example must define the `required_plugins` block. A pre-defined GitHub Action will run `packer init`, `packer validate`, and `packer build` to test your plugin with the latest version available of Packer. 4 | 5 | The folder can contain multiple HCL2 compatible files. The action will execute Packer at this folder level running `packer init -upgrade .` and `packer build .`. 6 | 7 | If the plugin requires authentication, the configuration should be provided via GitHub Secrets and set as environment variables in the [test-plugin-example.yml](/.github/workflows/test-plugin-example.yml) file. Example: 8 | 9 | ```yml 10 | - name: Build 11 | working-directory: ${{ github.event.inputs.folder }} 12 | run: PACKER_LOG=${{ github.event.inputs.logs }} packer build . 13 | env: 14 | AUTH_KEY: ${{ secrets.AUTH_KEY }} 15 | AUTH_PASSWORD: ${{ secrets.AUTH_PASSWORD }} 16 | ``` -------------------------------------------------------------------------------- /example/build.pkr.hcl: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | packer { 5 | required_plugins { 6 | qemu = { 7 | version = ">= 1.0.1" 8 | source = "github.com/hashicorp/qemu" 9 | } 10 | } 11 | } 12 | 13 | build { 14 | sources = ["source.qemu.example"] 15 | } 16 | 17 | source "qemu" "example" { 18 | boot_command = [ 19 | "", 20 | "", 21 | "", 22 | "", 23 | "", 24 | "", 25 | "", 26 | "", 27 | "", 28 | "", 29 | "/install/vmlinuz noapic ", 30 | "file=/floppy/preseed.cfg ", 31 | "debian-installer=en_US auto locale=en_US kbd-chooser/method=us ", 32 | "hostname=vagrant ", 33 | "fb=false debconf/frontend=noninteractive ", 34 | "keyboard-configuration/modelcode=SKIP ", 35 | "keyboard-configuration/layout=USA ", 36 | "keyboard-configuration/variant=USA console-setup/ask_detect=false ", 37 | "passwd/user-fullname=vagrant ", 38 | "passwd/user-password=vagrant ", 39 | "passwd/user-password-again=vagrant ", 40 | "passwd/username=vagrant ", 41 | "initrd=/install/initrd.gz -- " 42 | ] 43 | disk_size = "32768" 44 | floppy_files = ["http/preseed.cfg"] 45 | iso_urls = [ 46 | "http://releases.ubuntu.com/16.04/ubuntu-16.04.7-server-amd64.iso" 47 | ] 48 | iso_checksum = "sha256:b23488689e16cad7a269eb2d3a3bf725d3457ee6b0868e00c8762d3816e25848" 49 | output_directory = "output-ubuntu1804" 50 | shutdown_command = "echo 'vagrant'|sudo -S shutdown -P now" 51 | ssh_password = "vagrant" 52 | ssh_username = "vagrant" 53 | ssh_wait_timeout = "10000s" 54 | vm_name = "ubuntu1804" 55 | use_default_display = true 56 | } 57 | -------------------------------------------------------------------------------- /example/efi_build/efi-debian.pkr.hcl: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | source "qemu" "debian_efi" { 5 | iso_url = "https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/debian-11.5.0-amd64-netinst.iso" 6 | iso_checksum = "sha256:e307d0e583b4a8f7e5b436f8413d4707dd4242b70aea61eb08591dc0378522f3" 7 | communicator = "ssh" 8 | ssh_username = "root" 9 | ssh_password = "root" 10 | ssh_timeout = "30m" 11 | output_directory = "./out" 12 | memory = "1024" 13 | disk_size = "6G" 14 | cpus = 4 15 | format = "qcow2" 16 | accelerator = "kvm" 17 | vm_name = "debian_efi" 18 | # headless = "false" # uncomment to see the boot process in a qemu window 19 | machine_type = "q35" # As of now, q35 is required for secure boot to be enabled 20 | # Refer to the boot_steps attribute for more information on usage https://developer.hashicorp.com/packer/plugins/builders/qemu#boot_steps 21 | boot_steps = [ 22 | ["FS0:EFI\\boot\\bootx64.efi", "boot from EFI shell"], 23 | ["", "manual install"], 24 | ["", "automatic install"], 25 | ["", "wait 30s for preseed prompt"], 26 | ["http://{{.HTTPIP}}:{{.HTTPPort}}/preseed.cfg", "select preseed medium"], 27 | ["", "select English as language/locale"], 28 | ["", "select English as language"], 29 | ["", "set English-US as keyboard layout"], 30 | ["root", "set root password"], 31 | ["root", "confirm root password"], 32 | ["debian", "set machine name to debian"], 33 | ["", "set user to debian"], 34 | ["debian", "set password to debian"], 35 | ["debian", "confirm password to debian"], 36 | ["", "wait 3m for system to install"], 37 | ["rootrootsed -Ei 's/^#.*PermitRootLogin.*$/PermitRootLogin yes/' /etc/ssh/sshd_configsystemctl restart sshdexit", "configure sshd to allow root connection"], 38 | ] 39 | http_directory = "http" 40 | boot_wait = "3s" 41 | cpu_model = "host" 42 | qemuargs = [ 43 | ["-vga","virtio"] # if vga is not virtio, output is garbled for some reason 44 | ] 45 | vtpm = true 46 | efi_firmware_code = "./efi_data/OVMF_CODE_4M.ms.fd" 47 | efi_firmware_vars = "./efi_data/OVMF_VARS_4M.ms.fd" 48 | } 49 | 50 | build { 51 | sources = ["source.qemu.debian_efi"] 52 | 53 | provisioner "shell" { 54 | inline = [ "dmesg | grep -qi 'Secure boot enabled' && echo \"Secure Boot is on!\"" ] 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /example/efi_build/efi_data/OVMF_CODE_4M.ms.fd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp/packer-plugin-qemu/6203408f223310dc616379c7ae7665a840d5f792/example/efi_build/efi_data/OVMF_CODE_4M.ms.fd -------------------------------------------------------------------------------- /example/efi_build/efi_data/OVMF_VARS_4M.ms.fd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp/packer-plugin-qemu/6203408f223310dc616379c7ae7665a840d5f792/example/efi_build/efi_data/OVMF_VARS_4M.ms.fd -------------------------------------------------------------------------------- /example/efi_build/http/preseed.cfg: -------------------------------------------------------------------------------- 1 | choose-mirror-bin mirror/http/proxy string 2 | d-i debian-installer/framebuffer boolean false 3 | d-i debconf/frontend select noninteractive 4 | d-i base-installer/kernel/override-image string linux-server 5 | d-i clock-setup/utc boolean true 6 | d-i clock-setup/utc-auto boolean true 7 | d-i finish-install/reboot_in_progress note 8 | d-i grub-installer/only_debian boolean true 9 | d-i grub-installer/with_other_os boolean true 10 | d-i partman-auto/method string regular 11 | d-i partman/choose_partition select finish 12 | d-i partman/confirm boolean true 13 | d-i partman/confirm_nooverwrite boolean true 14 | d-i partman/confirm_write_new_label boolean true 15 | d-i pkgsel/include string openssh-server 16 | d-i pkgsel/install-language-support boolean false 17 | d-i pkgsel/update-policy select none 18 | d-i pkgsel/upgrade select full-upgrade 19 | d-i time/zone string UTC 20 | d-i user-setup/allow-password-weak boolean true 21 | d-i user-setup/encrypt-home boolean false 22 | tasksel tasksel/first multiselect standard, ubuntu-server -------------------------------------------------------------------------------- /example/http/preseed.cfg: -------------------------------------------------------------------------------- 1 | choose-mirror-bin mirror/http/proxy string 2 | d-i debian-installer/framebuffer boolean false 3 | d-i debconf/frontend select noninteractive 4 | d-i base-installer/kernel/override-image string linux-server 5 | d-i clock-setup/utc boolean true 6 | d-i clock-setup/utc-auto boolean true 7 | d-i finish-install/reboot_in_progress note 8 | d-i grub-installer/only_debian boolean true 9 | d-i grub-installer/with_other_os boolean true 10 | d-i partman-auto/method string regular 11 | d-i partman/choose_partition select finish 12 | d-i partman/confirm boolean true 13 | d-i partman/confirm_nooverwrite boolean true 14 | d-i partman/confirm_write_new_label boolean true 15 | d-i pkgsel/include string openssh-server 16 | d-i pkgsel/install-language-support boolean false 17 | d-i pkgsel/update-policy select none 18 | d-i pkgsel/upgrade select full-upgrade 19 | d-i time/zone string UTC 20 | d-i user-setup/allow-password-weak boolean true 21 | d-i user-setup/encrypt-home boolean false 22 | tasksel tasksel/first multiselect standard, ubuntu-server -------------------------------------------------------------------------------- /example/source.pkr.hcl: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | source "qemu" "example" { 5 | boot_command = [ 6 | "", 7 | "", 8 | "", 9 | "", 10 | "", 11 | "", 12 | "", 13 | "", 14 | "", 15 | "", 16 | "/install/vmlinuz noapic ", 17 | "file=/floppy/preseed.cfg ", 18 | "debian-installer=en_US auto locale=en_US kbd-chooser/method=us ", 19 | "hostname=vagrant ", 20 | "fb=false debconf/frontend=noninteractive ", 21 | "keyboard-configuration/modelcode=SKIP ", 22 | "keyboard-configuration/layout=USA ", 23 | "keyboard-configuration/variant=USA console-setup/ask_detect=false ", 24 | "passwd/user-fullname=vagrant ", 25 | "passwd/user-password=vagrant ", 26 | "passwd/user-password-again=vagrant ", 27 | "passwd/username=vagrant ", 28 | "initrd=/install/initrd.gz -- " 29 | ] 30 | disk_size = "32768" 31 | floppy_files = ["http/preseed.cfg"] 32 | iso_urls = [ 33 | "http://releases.ubuntu.com/16.04/ubuntu-16.04.7-server-amd64.iso" 34 | ] 35 | iso_checksum = "sha256:b23488689e16cad7a269eb2d3a3bf725d3457ee6b0868e00c8762d3816e25848" 36 | output_directory = "output-ubuntu1804" 37 | shutdown_command = "echo 'vagrant'|sudo -S shutdown -P now" 38 | ssh_password = "vagrant" 39 | ssh_username = "vagrant" 40 | ssh_wait_timeout = "10000s" 41 | vm_name = "ubuntu1804" 42 | use_default_display = true 43 | } -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/hashicorp/packer-plugin-qemu 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.7 6 | 7 | require ( 8 | github.com/aws/aws-sdk-go v1.44.114 9 | github.com/digitalocean/go-qemu v0.0.0-20210326154740-ac9e0b687001 10 | github.com/hashicorp/go-version v1.6.0 11 | github.com/hashicorp/hcl/v2 v2.19.1 12 | github.com/hashicorp/packer-plugin-sdk v0.6.1 13 | github.com/mitchellh/go-vnc v0.0.0-20150629162542-723ed9867aed 14 | github.com/stretchr/testify v1.10.0 15 | github.com/zclconf/go-cty v1.13.3 16 | ) 17 | 18 | require ( 19 | cloud.google.com/go v0.110.8 // indirect 20 | cloud.google.com/go/compute v1.23.1 // indirect 21 | cloud.google.com/go/compute/metadata v0.2.3 // indirect 22 | cloud.google.com/go/iam v1.1.3 // indirect 23 | cloud.google.com/go/storage v1.35.1 // indirect 24 | github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c // indirect 25 | github.com/ChrisTrenkamp/goxpath v0.0.0-20210404020558-97928f7e12b6 // indirect 26 | github.com/agext/levenshtein v1.2.3 // indirect 27 | github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect 28 | github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect 29 | github.com/armon/go-metrics v0.4.1 // 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/digitalocean/go-libvirt v0.0.0-20201209184759-e2a69bcd5bd1 // indirect 34 | github.com/dylanmei/iso8601 v0.1.0 // indirect 35 | github.com/fatih/color v1.16.0 // indirect 36 | github.com/go-jose/go-jose/v4 v4.0.5 // indirect 37 | github.com/gofrs/flock v0.8.1 // indirect 38 | github.com/gofrs/uuid v4.0.0+incompatible // indirect 39 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 40 | github.com/golang/protobuf v1.5.3 // indirect 41 | github.com/google/s2a-go v0.1.7 // indirect 42 | github.com/google/uuid v1.4.0 // indirect 43 | github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect 44 | github.com/googleapis/gax-go/v2 v2.12.0 // indirect 45 | github.com/hashicorp/consul/api v1.25.1 // indirect 46 | github.com/hashicorp/errwrap v1.1.0 // indirect 47 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 48 | github.com/hashicorp/go-getter/gcs/v2 v2.2.2 // indirect 49 | github.com/hashicorp/go-getter/s3/v2 v2.2.2 // indirect 50 | github.com/hashicorp/go-getter/v2 v2.2.2 // indirect 51 | github.com/hashicorp/go-hclog v1.6.3 // indirect 52 | github.com/hashicorp/go-immutable-radix v1.3.1 // indirect 53 | github.com/hashicorp/go-multierror v1.1.1 // indirect 54 | github.com/hashicorp/go-retryablehttp v0.7.7 // indirect 55 | github.com/hashicorp/go-rootcerts v1.0.2 // indirect 56 | github.com/hashicorp/go-safetemp v1.0.0 // indirect 57 | github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 // indirect 58 | github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect 59 | github.com/hashicorp/go-sockaddr v1.0.7 // indirect 60 | github.com/hashicorp/golang-lru v0.5.4 // indirect 61 | github.com/hashicorp/hcl v1.0.0 // indirect 62 | github.com/hashicorp/serf v0.10.1 // indirect 63 | github.com/hashicorp/vault/api v1.14.0 // indirect 64 | github.com/hashicorp/yamux v0.1.1 // indirect 65 | github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869 // indirect 66 | github.com/jmespath/go-jmespath v0.4.0 // indirect 67 | github.com/klauspost/compress v1.11.2 // indirect 68 | github.com/kr/fs v0.1.0 // indirect 69 | github.com/masterzen/simplexml v0.0.0-20190410153822-31eea3082786 // indirect 70 | github.com/masterzen/winrm v0.0.0-20210623064412-3b76017826b0 // indirect 71 | github.com/mattn/go-colorable v0.1.13 // indirect 72 | github.com/mattn/go-isatty v0.0.20 // indirect 73 | github.com/mitchellh/go-fs v0.0.0-20180402235330-b7b9ca407fff // indirect 74 | github.com/mitchellh/go-homedir v1.1.0 // indirect 75 | github.com/mitchellh/go-testing-interface v1.14.1 // indirect 76 | github.com/mitchellh/go-wordwrap v1.0.1 // indirect 77 | github.com/mitchellh/iochan v1.0.0 // indirect 78 | github.com/mitchellh/mapstructure v1.5.0 // indirect 79 | github.com/mitchellh/reflectwalk v1.0.0 // indirect 80 | github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect 81 | github.com/packer-community/winrmcp v0.0.0-20180921211025-c76d91c1e7db // indirect 82 | github.com/pkg/sftp v1.13.2 // indirect 83 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 84 | github.com/ryanuber/go-glob v1.0.0 // indirect 85 | github.com/ugorji/go/codec v1.2.6 // indirect 86 | github.com/ulikunitz/xz v0.5.10 // indirect 87 | github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect 88 | github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 89 | go.opencensus.io v0.24.0 // indirect 90 | golang.org/x/crypto v0.36.0 // indirect 91 | golang.org/x/exp v0.0.0-20230321023759-10a507213a29 // indirect 92 | golang.org/x/mobile v0.0.0-20210901025245-1fde1d6c3ca1 // indirect 93 | golang.org/x/net v0.37.0 // indirect 94 | golang.org/x/oauth2 v0.13.0 // indirect 95 | golang.org/x/sync v0.12.0 // indirect 96 | golang.org/x/sys v0.31.0 // indirect 97 | golang.org/x/term v0.30.0 // indirect 98 | golang.org/x/text v0.23.0 // indirect 99 | golang.org/x/time v0.11.0 // indirect 100 | golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect 101 | google.golang.org/api v0.150.0 // indirect 102 | google.golang.org/appengine v1.6.7 // indirect 103 | google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b // indirect 104 | google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b // indirect 105 | google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405 // indirect 106 | google.golang.org/grpc v1.59.0 // indirect 107 | google.golang.org/protobuf v1.33.0 // indirect 108 | gopkg.in/yaml.v2 v2.3.0 // indirect 109 | gopkg.in/yaml.v3 v3.0.1 // indirect 110 | ) 111 | 112 | 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 113 | -------------------------------------------------------------------------------- /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 | "github.com/hashicorp/packer-plugin-qemu/builder/qemu" 13 | "github.com/hashicorp/packer-plugin-qemu/version" 14 | ) 15 | 16 | func main() { 17 | pps := plugin.NewSet() 18 | pps.RegisterBuilder(plugin.DEFAULT_NAME, new(qemu.Builder)) 19 | pps.SetVersion(version.PluginVersion) 20 | err := pps.Run() 21 | if err != nil { 22 | fmt.Fprintln(os.Stderr, err.Error()) 23 | os.Exit(1) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /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 is the main version number that is being run at the moment. 10 | Version = "1.1.3" 11 | 12 | // VersionPrerelease is A pre-release marker for the Version. If this is "" 13 | // (empty string) then it means that it is a final release. Otherwise, this 14 | // is a pre-release such as "dev" (in development), "beta", "rc1", etc. 15 | VersionPrerelease = "dev" 16 | 17 | // PluginVersion is used by the plugin set to allow Packer to recognize 18 | // what version this plugin is. 19 | PluginVersion = version.InitializePluginVersion(Version, VersionPrerelease) 20 | ) 21 | --------------------------------------------------------------------------------