├── .github ├── ISSUE_TEMPLATE │ ├── anything.md │ └── config.yml └── contributing.md ├── .gitignore ├── .idea └── dictionaries │ └── project.xml ├── .teamcity ├── .gitignore ├── patches │ ├── buildTypes │ │ └── Build.kts │ └── projects │ │ └── _Self.kts ├── pom.xml └── settings.kts ├── LICENSE.txt ├── Makefile ├── README.md ├── clone ├── builder.go ├── builder_acc_test.go ├── builder_test.go ├── config.go ├── config_test.go ├── leak_test.go └── step_clone.go ├── cmd ├── clone │ └── main.go └── iso │ └── main.go ├── common ├── artifact.go ├── config_location.go ├── config_ssh.go ├── step_config_params.go ├── step_connect.go ├── step_hardware.go ├── step_run.go ├── step_shutdown.go ├── step_snapshot.go ├── step_template.go ├── step_wait_for_ip.go └── testing │ └── utility.go ├── docker-compose.yml ├── driver ├── datastore.go ├── datastore_acc_test.go ├── driver.go ├── driver_test.go ├── folder.go ├── folder_acc_test.go ├── host.go ├── host_acc_test.go ├── leak_test.go ├── network.go ├── resource_pool.go ├── resource_pool_acc_test.go ├── vm.go ├── vm_cdrom.go ├── vm_clone_acc_test.go ├── vm_create_acc_test.go └── vm_keyboard.go ├── examples ├── alpine │ ├── alpine-3.8.json │ ├── answerfile │ └── setup.sh ├── clone │ └── alpine.json ├── driver │ └── main.go ├── macos │ ├── macos-10.13.json │ └── setup │ │ ├── .gitignore │ │ ├── iso-macos.sh │ │ ├── iso-setup.sh │ │ ├── postinstall │ │ └── setup.sh ├── ubuntu │ ├── preseed.cfg │ └── ubuntu-16.04.json └── windows │ ├── .gitattributes │ ├── setup │ ├── Autounattend.xml │ ├── setup.ps1 │ └── vmtools.cmd │ └── windows-10.json ├── go.mod ├── go.sum ├── gofmt.sh ├── iso ├── builder.go ├── builder_acc_test.go ├── config.go ├── leak_test.go ├── step_add_cdrom.go ├── step_add_floppy.go ├── step_boot_command.go ├── step_create.go ├── step_remote_upload.go ├── step_remove_cdrom.go └── step_remove_floppy.go ├── teamcity-services.yml └── test ├── lab.ovpn ├── lab.p12 ├── test-key.pem └── test-key.pub /.github/ISSUE_TEMPLATE/anything.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Anything 3 | about: Any issue or feature suggestion 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | # Deprecation notice 11 | This plugin was merged into [official Packer repository](https://github.com/hashicorp/packer) 12 | and released with Packer since version 1.5.2. 13 | 14 | Please use the newest version of Packer and report problems, feature suggestions 15 | to [main Packer issue tracker](https://github.com/hashicorp/packer/issues) 16 | according to [their contributing policies](https://github.com/hashicorp/packer/blob/master/.github/CONTRIBUTING.md). 17 | 18 | Issues opened here may be left unanswered for weeks if answered at all. 19 | 20 | This repository left for history and not intended for further use. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.github/contributing.md: -------------------------------------------------------------------------------- 1 | # Deprecation notice 2 | This plugin was merged into [official Packer repository](https://github.com/hashicorp/packer) 3 | and released with Packer since version 1.5.2. 4 | 5 | Please use the newest version of Packer and report problems, feature suggestions 6 | to [main Packer issue tracker](https://github.com/hashicorp/packer/issues) 7 | according to [their contributing policies](https://github.com/hashicorp/packer/blob/master/.github/CONTRIBUTING.md). 8 | 9 | Issues opened here may be left unanswered for weeks. 10 | 11 | This repository left for history and not intended for further use. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | packer-builder-vsphere* 3 | build/ 4 | bin/ 5 | .env 6 | test*.json 7 | crash.log 8 | packer_cache/ 9 | vendor/ 10 | -------------------------------------------------------------------------------- /.idea/dictionaries/project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | abcdefghijklmnopqrstuvwxyz 5 | cdrom 6 | cdroms 7 | datastore 8 | datastores 9 | esxi 10 | hashicorp 11 | mozilla 12 | sata 13 | scancode 14 | vcenter 15 | vmware 16 | vmxnet 17 | vsphere 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /.teamcity/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | target/ 3 | -------------------------------------------------------------------------------- /.teamcity/patches/buildTypes/Build.kts: -------------------------------------------------------------------------------- 1 | package patches.buildTypes 2 | 3 | import jetbrains.buildServer.configs.kotlin.v2018_2.* 4 | import jetbrains.buildServer.configs.kotlin.v2018_2.ui.* 5 | 6 | /* 7 | This patch script was generated by TeamCity on settings change in UI. 8 | To apply the patch, change the buildType with id = 'Build' 9 | accordingly, and delete the patch script. 10 | */ 11 | changeBuildType(RelativeId("Build")) { 12 | check(paused == false) { 13 | "Unexpected paused: '$paused'" 14 | } 15 | paused = true 16 | } 17 | -------------------------------------------------------------------------------- /.teamcity/patches/projects/_Self.kts: -------------------------------------------------------------------------------- 1 | package patches.projects 2 | 3 | import jetbrains.buildServer.configs.kotlin.v2018_2.* 4 | import jetbrains.buildServer.configs.kotlin.v2018_2.Project 5 | import jetbrains.buildServer.configs.kotlin.v2018_2.ui.* 6 | 7 | /* 8 | This patch script was generated by TeamCity on settings change in UI. 9 | To apply the patch, change the root project 10 | accordingly, and delete the patch script. 11 | */ 12 | changeProject(DslContext.projectId) { 13 | check(archived == false) { 14 | "Unexpected archived: '$archived'" 15 | } 16 | archived = true 17 | } 18 | -------------------------------------------------------------------------------- /.teamcity/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | PackerVSphere Config DSL Script 5 | PackerVSphere 6 | PackerVSphere_dsl 7 | 1.0-SNAPSHOT 8 | 9 | 10 | org.jetbrains.teamcity 11 | configs-dsl-kotlin-parent 12 | 1.0-SNAPSHOT 13 | 14 | 15 | 16 | 17 | jetbrains-all 18 | https://download.jetbrains.com/teamcity-repository 19 | 20 | true 21 | 22 | 23 | 24 | teamcity-server 25 | https://teamcity.jetbrains.com/app/dsl-plugins-repository 26 | 27 | true 28 | 29 | 30 | 31 | 32 | 33 | 34 | JetBrains 35 | https://download.jetbrains.com/teamcity-repository 36 | 37 | 38 | 39 | 40 | . 41 | 42 | 43 | kotlin-maven-plugin 44 | org.jetbrains.kotlin 45 | ${kotlin.version} 46 | 47 | 48 | 49 | 50 | compile 51 | process-sources 52 | 53 | compile 54 | 55 | 56 | 57 | test-compile 58 | process-test-sources 59 | 60 | test-compile 61 | 62 | 63 | 64 | 65 | 66 | org.jetbrains.teamcity 67 | teamcity-configs-maven-plugin 68 | ${teamcity.dsl.version} 69 | 70 | kotlin 71 | target/generated-configs 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | org.jetbrains.teamcity 80 | configs-dsl-kotlin 81 | ${teamcity.dsl.version} 82 | compile 83 | 84 | 85 | org.jetbrains.teamcity 86 | configs-dsl-kotlin-plugins 87 | 1.0-SNAPSHOT 88 | pom 89 | compile 90 | 91 | 92 | org.jetbrains.kotlin 93 | kotlin-stdlib-jdk8 94 | ${kotlin.version} 95 | compile 96 | 97 | 98 | org.jetbrains.kotlin 99 | kotlin-script-runtime 100 | ${kotlin.version} 101 | compile 102 | 103 | 104 | -------------------------------------------------------------------------------- /.teamcity/settings.kts: -------------------------------------------------------------------------------- 1 | import jetbrains.buildServer.configs.kotlin.v2018_2.* 2 | import jetbrains.buildServer.configs.kotlin.v2018_2.buildFeatures.PullRequests 3 | import jetbrains.buildServer.configs.kotlin.v2018_2.buildFeatures.commitStatusPublisher 4 | import jetbrains.buildServer.configs.kotlin.v2018_2.buildFeatures.pullRequests 5 | import jetbrains.buildServer.configs.kotlin.v2018_2.buildSteps.dockerCompose 6 | import jetbrains.buildServer.configs.kotlin.v2018_2.buildSteps.script 7 | import jetbrains.buildServer.configs.kotlin.v2018_2.triggers.vcs 8 | import jetbrains.buildServer.configs.kotlin.v2018_2.vcs.GitVcsRoot 9 | 10 | version = "2018.2" 11 | 12 | project { 13 | description = "https://github.com/jetbrains-infra/packer-builder-vsphere" 14 | 15 | vcsRoot(GitHub) 16 | buildType(Build) 17 | 18 | features { 19 | feature { 20 | type = "OAuthProvider" 21 | param("providerType", "GitHub") 22 | param("displayName", "GitHub.com") 23 | param("gitHubUrl", "https://github.com/") 24 | param("clientId", "1abfd46417d7795298a1") 25 | param("secure:clientSecret", "credentialsJSON:5fe99dc3-4d1d-4fd6-9f5c-e87fbcbd9a4e") 26 | param("defaultTokenScope", "public_repo,repo,repo:status,write:repo_hook") 27 | } 28 | feature { 29 | type = "IssueTracker" 30 | param("name", "packer-builder-vsphere") 31 | param("type", "GithubIssues") 32 | param("repository", "https://github.com/jetbrains-infra/packer-builder-vsphere") 33 | param("authType", "anonymous") 34 | param("pattern", """#(\d+)""") 35 | } 36 | } 37 | } 38 | 39 | object GitHub : GitVcsRoot({ 40 | name = "packer-builder-vsphere" 41 | url = "https://github.com/jetbrains-infra/packer-builder-vsphere" 42 | branch = "master" 43 | branchSpec = "+:refs/heads/(*)" 44 | userNameStyle = GitVcsRoot.UserNameStyle.FULL 45 | }) 46 | 47 | object Build : BuildType({ 48 | val golangImage = "jetbrainsinfra/golang:1.11.4" 49 | 50 | name = "Build" 51 | 52 | vcs { 53 | root(GitHub) 54 | } 55 | 56 | requirements { 57 | equals("docker.server.osType", "linux") 58 | exists("dockerCompose.version") 59 | 60 | doesNotContain("teamcity.agent.name", "ubuntu-single-build") 61 | } 62 | 63 | params { 64 | param("env.GOPATH", "%teamcity.build.checkoutDir%/build/modules") 65 | param("env.GOCACHE", "%teamcity.build.checkoutDir%/build/cache") 66 | 67 | password("env.VPN_PASSWORD", "credentialsJSON:8c355e81-9a26-4788-8fea-c854cd646c35") 68 | param ("env.VSPHERE_USERNAME", """vsphere65.test\teamcity""") 69 | password("env.VSPHERE_PASSWORD", "credentialsJSON:d5e7ac7f-357b-464a-b2fa-ddd4c433b22b") 70 | } 71 | 72 | steps { 73 | script { 74 | name = "Build" 75 | scriptContent = "make build -j 3" 76 | dockerImage = golangImage 77 | dockerPull = true 78 | } 79 | 80 | dockerCompose { 81 | name = "Start VPN tunnel" 82 | file = "teamcity-services.yml" 83 | } 84 | 85 | script { 86 | name = "Test" 87 | scriptContent = "make test | go-test-teamcity" 88 | dockerImage = golangImage 89 | dockerPull = true 90 | dockerRunParameters = "--network=container:vpn" 91 | } 92 | script { 93 | name = "gofmt" 94 | executionMode = BuildStep.ExecutionMode.RUN_ON_FAILURE 95 | scriptContent = "./gofmt.sh" 96 | dockerImage = golangImage 97 | dockerPull = true 98 | } 99 | } 100 | 101 | features { 102 | commitStatusPublisher { 103 | publisher = github { 104 | githubUrl = "https://api.github.com" 105 | authType = personalToken { 106 | token = "credentialsJSON:5ead3bb1-c370-4589-beb8-24f8d02c36bc" 107 | } 108 | } 109 | } 110 | pullRequests { 111 | provider = github { 112 | authType = token { 113 | token = "credentialsJSON:5ead3bb1-c370-4589-beb8-24f8d02c36bc" 114 | } 115 | filterAuthorRole = PullRequests.GitHubRoleFilter.EVERYBODY 116 | } 117 | } 118 | } 119 | 120 | triggers { 121 | vcs { 122 | triggerRules = """ 123 | -:*.md 124 | -:.teamcity/ 125 | """.trimIndent() 126 | branchFilter = """ 127 | +:* 128 | -:temp-* 129 | -:pull/* 130 | """.trimIndent() 131 | } 132 | } 133 | maxRunningBuilds = 2 134 | 135 | artifactRules = "bin/* => packer-builder-vsphere-%build.number%.zip" 136 | allowExternalStatus = true 137 | }) 138 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GOOPTS := GOARCH=amd64 CGO_ENABLED=0 2 | 3 | build: iso clone 4 | 5 | iso: iso-linux iso-windows iso-macos 6 | clone: clone-linux clone-windows clone-macos 7 | 8 | iso-linux: modules bin 9 | $(GOOPTS) GOOS=linux go build -o bin/packer-builder-vsphere-iso.linux ./cmd/iso 10 | 11 | iso-windows: modules bin 12 | $(GOOPTS) GOOS=windows go build -o bin/packer-builder-vsphere-iso.exe ./cmd/iso 13 | 14 | iso-macos: modules bin 15 | $(GOOPTS) GOOS=darwin go build -o bin/packer-builder-vsphere-iso.macos ./cmd/iso 16 | 17 | clone-linux: modules bin 18 | $(GOOPTS) GOOS=linux go build -o bin/packer-builder-vsphere-clone.linux ./cmd/clone 19 | 20 | clone-windows: modules bin 21 | $(GOOPTS) GOOS=windows go build -o bin/packer-builder-vsphere-clone.exe ./cmd/clone 22 | 23 | clone-macos: modules bin 24 | $(GOOPTS) GOOS=darwin go build -o bin/packer-builder-vsphere-clone.macos ./cmd/clone 25 | 26 | modules: 27 | go mod download 28 | 29 | bin: 30 | mkdir -p bin 31 | rm -f bin/* 32 | 33 | test: 34 | PACKER_ACC=1 go test -v -count 1 ./driver ./iso ./clone 35 | 36 | .PHONY: bin test 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Team project](http://jb.gg/badges/obsolete.svg)](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub) 2 | [![GitHub latest release](https://img.shields.io/github/release/jetbrains-infra/packer-builder-vsphere.svg)](https://github.com/jetbrains-infra/packer-builder-vsphere/releases) 3 | [![GitHub downloads](https://img.shields.io/github/downloads/jetbrains-infra/packer-builder-vsphere/total.svg)](https://github.com/jetbrains-infra/packer-builder-vsphere/releases) 4 | [![TeamCity build status](https://img.shields.io/teamcity/http/teamcity.jetbrains.com/s/PackerVSphere_Build.svg)](https://teamcity.jetbrains.com/viewType.html?buildTypeId=PackerVSphere_Build&guest=1) 5 | 6 | # Deprecation notice 7 | This plugin was merged into [official Packer repository](https://github.com/hashicorp/packer) and released with Packer since version 1.5.2. 8 | 9 | Please use modern version of Packer and report problems, feature suggestions to main Packer repository. 10 | 11 | This repository left for history and archived. 12 | 13 | 14 | # Packer Builder for VMware vSphere 15 | 16 | This a plugin for [HashiCorp Packer](https://www.packer.io/). It uses native vSphere API, and creates virtual machines remotely. 17 | 18 | `vsphere-iso` builder creates new VMs from scratch. 19 | `vsphere-clone` builder clones VMs from existing templates. 20 | 21 | - VMware Player is not required. 22 | - Official vCenter API is used, no ESXi host [modification](https://www.packer.io/docs/builders/vmware-iso.html#building-on-a-remote-vsphere-hypervisor) is required. 23 | 24 | ## Installation 25 | * Download binaries from the [releases page](https://github.com/jetbrains-infra/packer-builder-vsphere/releases). 26 | * [Install](https://www.packer.io/docs/extending/plugins.html#installing-plugins) the plugins, or simply put them into the same directory with JSON templates. On Linux and macOS run `chmod +x` on the files. 27 | 28 | ## Build 29 | 30 | Install Go and [dep](https://github.com/golang/dep/releases), run `build.sh`. 31 | 32 | Or build inside a container by Docker Compose: 33 | ``` 34 | docker-compose run build 35 | ``` 36 | 37 | The binaries will be in `bin/` directory. 38 | 39 | Artifacts can be also downloaded from [TeamCity builds](https://teamcity.jetbrains.com/viewLog.html?buildTypeId=PackerVSphere_Build&buildId=lastSuccessful&tab=artifacts&guest=1). 40 | 41 | ## Examples 42 | 43 | See complete Ubuntu, Windows, and macOS templates in the [examples folder](https://github.com/jetbrains-infra/packer-builder-vsphere/tree/master/examples/). 44 | 45 | ## Parameter Reference 46 | 47 | ### Connection 48 | 49 | * `vcenter_server`(string) - vCenter server hostname. 50 | * `username`(string) - vSphere username. 51 | * `password`(string) - vSphere password. 52 | * `insecure_connection`(boolean) - Do not validate vCenter server's TLS certificate. Defaults to `false`. 53 | * `datacenter`(string) - VMware datacenter name. Required if there is more than one datacenter in vCenter. 54 | 55 | ### VM Location 56 | 57 | * `vm_name`(string) - Name of the new VM to create. 58 | * `folder`(string) - VM folder to create the VM in. 59 | * `host`(string) - ESXi host where target VM is created. A full path must be specified if the host is in a folder. For example `folder/host`. See the `Specifying Clusters and Hosts` section above for more details. 60 | * `cluster`(string) - ESXi cluster where target VM is created. See [Working with Clusters](#working-with-clusters). 61 | * `resource_pool`(string) - VMWare resource pool. Defaults to the root resource pool of the `host` or `cluster`. 62 | * `datastore`(string) - VMWare datastore. Required if `host` is a cluster, or if `host` has multiple datastores. 63 | * `notes`(string) - VM notes. 64 | 65 | ### VM Location (`vsphere-clone` only) 66 | 67 | * `template`(string) - Name of source VM. Path is optional. 68 | * `linked_clone`(boolean) - Create VM as a linked clone from latest snapshot. Defaults to `false`. 69 | 70 | ### Hardware 71 | 72 | * `CPUs`(number) - Number of CPU sockets. 73 | * `cpu_cores`(number) - Number of CPU cores per socket. 74 | * `CPU_limit`(number) - Upper limit of available CPU resources in MHz. 75 | * `CPU_reservation`(number) - Amount of reserved CPU resources in MHz. 76 | * `CPU_hot_plug`(boolean) - Enable CPU hot plug setting for virtual machine. Defaults to `false`. 77 | * `RAM`(number) - Amount of RAM in MB. 78 | * `RAM_reservation`(number) - Amount of reserved RAM in MB. 79 | * `RAM_reserve_all`(boolean) - Reserve all available RAM. Defaults to `false`. Cannot be used together with `RAM_reservation`. 80 | * `RAM_hot_plug`(boolean) - Enable RAM hot plug setting for virtual machine. Defaults to `false`. 81 | * `video_ram`(number) - Amount of video memory in MB. 82 | * `disk_size`(number) - The size of the disk in MB. 83 | * `network`(string) - Set network VM will be connected to. 84 | * `NestedHV`(boolean) - Enable nested hardware virtualization for VM. Defaults to `false`. 85 | * `configuration_parameters`(map) - Custom parameters. 86 | * `boot_order`(string) - Priority of boot devices. Defaults to `disk,cdrom` 87 | 88 | ### Hardware (`vsphere-iso` only) 89 | 90 | * `vm_version`(number) - Set VM hardware version. Defaults to the most current VM hardware version supported by vCenter. See [VMWare article 1003746](https://kb.vmware.com/s/article/1003746) for the full list of supported VM hardware versions. 91 | * `guest_os_type`(string) - Set VM OS type. Defaults to `otherGuest`. See [here](https://pubs.vmware.com/vsphere-6-5/index.jsp?topic=%2Fcom.vmware.wssdk.apiref.doc%2Fvim.vm.GuestOsDescriptor.GuestOsIdentifier.html) for a full list of possible values. 92 | * `disk_controller_type`(string) - Set VM disk controller type. Example `pvscsi`. 93 | * `disk_thin_provisioned`(boolean) - Enable VMDK thin provisioning for VM. Defaults to `false`. 94 | * `network_card`(string) - Set VM network card type. Example `vmxnet3`. 95 | * `usb_controller`(boolean) - Create USB controller for virtual machine. Defaults to `false`. 96 | * `cdrom_type`(string) - Which controller to use. Example `sata`. Defaults to `ide`. 97 | * `firmware`(string) - Set the Firmware at machine creation. Example `efi`. Defaults to `bios`. 98 | 99 | 100 | ### Boot (`vsphere-iso` only) 101 | 102 | * `iso_paths`(array of strings) - List of datastore paths to ISO files that will be mounted to the VM. Example `"[datastore1] ISO/ubuntu.iso"`. 103 | * `floppy_files`(array of strings) - List of local files to be mounted to the VM floppy drive. Can be used to make Debian preseed or RHEL kickstart files available to the VM. 104 | * `floppy_dirs`(array of strings) - List of directories to copy files from. 105 | * `floppy_img_path`(string) - Datastore path to a floppy image that will be mounted to the VM. Example `[datastore1] ISO/pvscsi-Windows8.flp`. 106 | * `http_directory`(string) - Path to a directory to serve using a local HTTP server. Beware of [limitations](https://github.com/jetbrains-infra/packer-builder-vsphere/issues/108#issuecomment-449634324). 107 | * `http_ip`(string) - Specify IP address on which the HTTP server is started. If not provided the first non-loopback interface is used. 108 | * `http_port_min` and `http_port_max` as in other [builders](https://www.packer.io/docs/builders/virtualbox-iso.html#http_port_min). 109 | * `iso_urls`(array of strings) - Multiple URLs for the ISO to download. Packer will try these in order. If anything goes wrong attempting to download or while downloading a single URL, it will move on to the next. All URLs must point to the same file (same checksum). By default this is empty and iso_url is used. Only one of iso_url or iso_urls can be specified. 110 | * `iso_checksum `(string) - The checksum for the OS ISO file. Because ISO files are so large, this is required and Packer will verify it prior to booting a virtual machine with the ISO attached. The type of the checksum is specified with iso_checksum_type, documented below. At least one of iso_checksum and iso_checksum_url must be defined. This has precedence over iso_checksum_url type. 111 | * `iso_checksum_type`(string) - The type of the checksum specified in iso_checksum. Valid values are none, md5, sha1, sha256, or sha512 currently. While none will skip checksumming, this is not recommended since ISO files are generally large and corruption does happen from time to time. 112 | * `iso_checksum_url`(string) - A URL to a GNU or BSD style checksum file containing a checksum for the OS ISO file. At least one of iso_checksum and iso_checksum_url must be defined. This will be ignored if iso_checksum is non empty. 113 | * `boot_wait`(string) Amount of time to wait for the VM to boot. Examples 45s and 10m. Defaults to 10 seconds. See [format](https://golang.org/pkg/time/#ParseDuration). 114 | * `boot_command`(array of strings) - List of commands to type when the VM is first booted. Used to initalize the operating system installer. See details in [Packer docs](https://www.packer.io/docs/builders/virtualbox-iso.html#boot-command). 115 | 116 | ### Provision 117 | 118 | * `communicator` - `ssh` (default), `winrm`, or `none` (create/clone, customize hardware, but do not boot). 119 | * `ip_wait_timeout`(string) - Amount of time to wait for VM's IP, similar to 'ssh_timeout'. Defaults to 30m (30 minutes). See the Go Lang [ParseDuration](https://golang.org/pkg/time/#ParseDuration) documentation for full details. 120 | * `ip_settle_timeout`(string) - Amount of time to wait for VM's IP to settle down, sometimes VM may report incorrect IP initially, then its recommended to set that parameter to apx. 2 minutes. Examples 45s and 10m. Defaults to 5s(5 seconds). See the Go Lang [ParseDuration](https://golang.org/pkg/time/#ParseDuration) documentation for full details. 121 | * `ssh_username`(string) - Username in guest OS. 122 | * `ssh_password`(string) - Password to access guest OS. Only specify `ssh_password` or `ssh_private_key_file`, but not both. 123 | * `ssh_private_key_file`(string) - Path to the SSH private key file to access guest OS. Only specify `ssh_password` or `ssh_private_key_file`, but not both. 124 | * `winrm_username`(string) - Username in guest OS. 125 | * `winrm_password`(string) - Password to access guest OS. 126 | * `shutdown_command`(string) - Specify a VM guest shutdown command. VMware guest tools are used by default. 127 | * `shutdown_timeout`(string) - Amount of time to wait for graceful VM shutdown. Examples 45s and 10m. Defaults to 5m(5 minutes). See the Go Lang [ParseDuration](https://golang.org/pkg/time/#ParseDuration) documentation for full details. 128 | 129 | ### Postprocessing 130 | 131 | * `create_snapshot`(boolean) - Create a snapshot when set to `true`, so the VM can be used as a base for linked clones. Defaults to `false`. 132 | * `convert_to_template`(boolean) - Convert VM to a template. Defaults to `false`. 133 | 134 | ## Working with Clusters 135 | #### Standalone Hosts 136 | Only use the `host` option. Optionally specify a `resource_pool`: 137 | ``` 138 | "host": "esxi-1.vsphere65.test", 139 | "resource_pool": "pool1", 140 | ``` 141 | 142 | #### Clusters Without DRS 143 | Use the `cluster` and `host `parameters: 144 | ``` 145 | "cluster": "cluster1", 146 | "host": "esxi-2.vsphere65.test", 147 | ``` 148 | 149 | #### Clusters With DRS 150 | Only use the `cluster` option. Optionally specify a `resource_pool`: 151 | ``` 152 | "cluster": "cluster2", 153 | "resource_pool": "pool1", 154 | ``` 155 | 156 | ## Required vSphere Permissions 157 | 158 | * VM folder (this object and children): 159 | ``` 160 | Virtual machine -> Inventory 161 | Virtual machine -> Configuration 162 | Virtual machine -> Interaction 163 | Virtual machine -> Snapshot management 164 | Virtual machine -> Provisioning 165 | ``` 166 | Individual privileges are listed in https://github.com/jetbrains-infra/packer-builder-vsphere/issues/97#issuecomment-436063235. 167 | * Resource pool, host, or cluster (this object): 168 | ``` 169 | Resource -> Assign virtual machine to resource pool 170 | ``` 171 | * Host in clusters without DRS (this object): 172 | ``` 173 | Read-only 174 | ``` 175 | * Datastore (this object): 176 | ``` 177 | Datastore -> Allocate space 178 | Datastore -> Browse datastore 179 | Datastore -> Low level file operations 180 | ``` 181 | * Network (this object): 182 | ``` 183 | Network -> Assign network 184 | ``` 185 | * Distributed switch (this object): 186 | ``` 187 | Read-only 188 | ``` 189 | 190 | For floppy image upload: 191 | 192 | * Datacenter (this object): 193 | ``` 194 | Datastore -> Low level file operations 195 | ``` 196 | * Host (this object): 197 | ``` 198 | Host -> Configuration -> System Management 199 | ``` 200 | -------------------------------------------------------------------------------- /clone/builder.go: -------------------------------------------------------------------------------- 1 | package clone 2 | 3 | import ( 4 | "context" 5 | packerCommon "github.com/hashicorp/packer/common" 6 | "github.com/hashicorp/packer/helper/communicator" 7 | "github.com/hashicorp/packer/helper/multistep" 8 | "github.com/hashicorp/packer/packer" 9 | "github.com/jetbrains-infra/packer-builder-vsphere/common" 10 | "github.com/jetbrains-infra/packer-builder-vsphere/driver" 11 | ) 12 | 13 | type Builder struct { 14 | config *Config 15 | runner multistep.Runner 16 | } 17 | 18 | func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { 19 | c, warnings, errs := NewConfig(raws...) 20 | if errs != nil { 21 | return warnings, errs 22 | } 23 | b.config = c 24 | 25 | return warnings, nil 26 | } 27 | 28 | func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (packer.Artifact, error) { 29 | state := new(multistep.BasicStateBag) 30 | state.Put("comm", &b.config.Comm) 31 | state.Put("hook", hook) 32 | state.Put("ui", ui) 33 | 34 | var steps []multistep.Step 35 | 36 | steps = append(steps, 37 | &common.StepConnect{ 38 | Config: &b.config.ConnectConfig, 39 | }, 40 | &StepCloneVM{ 41 | Config: &b.config.CloneConfig, 42 | Location: &b.config.LocationConfig, 43 | Force: b.config.PackerConfig.PackerForce, 44 | }, 45 | &common.StepConfigureHardware{ 46 | Config: &b.config.HardwareConfig, 47 | }, 48 | &common.StepConfigParams{ 49 | Config: &b.config.ConfigParamsConfig, 50 | }, 51 | ) 52 | 53 | if b.config.Comm.Type != "none" { 54 | steps = append(steps, 55 | &common.StepRun{ 56 | Config: &b.config.RunConfig, 57 | SetOrder: false, 58 | }, 59 | &common.StepWaitForIp{ 60 | Config: &b.config.WaitIpConfig, 61 | }, 62 | &communicator.StepConnect{ 63 | Config: &b.config.Comm, 64 | Host: common.CommHost(b.config.Comm.SSHHost), 65 | SSHConfig: b.config.Comm.SSHConfigFunc(), 66 | }, 67 | &packerCommon.StepProvision{}, 68 | &common.StepShutdown{ 69 | Config: &b.config.ShutdownConfig, 70 | }, 71 | ) 72 | } 73 | 74 | steps = append(steps, 75 | &common.StepCreateSnapshot{ 76 | CreateSnapshot: b.config.CreateSnapshot, 77 | }, 78 | &common.StepConvertToTemplate{ 79 | ConvertToTemplate: b.config.ConvertToTemplate, 80 | }, 81 | ) 82 | 83 | b.runner = packerCommon.NewRunner(steps, b.config.PackerConfig, ui) 84 | b.runner.Run(ctx, state) 85 | 86 | if rawErr, ok := state.GetOk("error"); ok { 87 | return nil, rawErr.(error) 88 | } 89 | 90 | if _, ok := state.GetOk("vm"); !ok { 91 | return nil, nil 92 | } 93 | artifact := &common.Artifact{ 94 | Name: b.config.VMName, 95 | VM: state.Get("vm").(*driver.VirtualMachine), 96 | } 97 | return artifact, nil 98 | } 99 | -------------------------------------------------------------------------------- /clone/builder_acc_test.go: -------------------------------------------------------------------------------- 1 | package clone 2 | 3 | import ( 4 | builderT "github.com/hashicorp/packer/helper/builder/testing" 5 | "github.com/hashicorp/packer/packer" 6 | "github.com/jetbrains-infra/packer-builder-vsphere/common" 7 | commonT "github.com/jetbrains-infra/packer-builder-vsphere/common/testing" 8 | "github.com/vmware/govmomi/vim25/types" 9 | "os" 10 | "testing" 11 | ) 12 | 13 | func TestCloneBuilderAcc_default(t *testing.T) { 14 | config := defaultConfig() 15 | builderT.Test(t, builderT.TestCase{ 16 | Builder: &Builder{}, 17 | Template: commonT.RenderConfig(config), 18 | Check: checkDefault(t, config["vm_name"].(string), config["host"].(string), "datastore1"), 19 | }) 20 | } 21 | 22 | func defaultConfig() map[string]interface{} { 23 | username := os.Getenv("VSPHERE_USERNAME") 24 | if username == "" { 25 | username = "root" 26 | } 27 | password := os.Getenv("VSPHERE_PASSWORD") 28 | if password == "" { 29 | password = "jetbrains" 30 | } 31 | 32 | config := map[string]interface{}{ 33 | "vcenter_server": "vcenter.vsphere65.test", 34 | "username": username, 35 | "password": password, 36 | "insecure_connection": true, 37 | 38 | "template": "alpine", 39 | "host": "esxi-1.vsphere65.test", 40 | 41 | "linked_clone": true, // speed up 42 | "communicator": "none", 43 | } 44 | config["vm_name"] = commonT.NewVMName() 45 | return config 46 | } 47 | 48 | func checkDefault(t *testing.T, name string, host string, datastore string) builderT.TestCheckFunc { 49 | return func(artifacts []packer.Artifact) error { 50 | d := commonT.TestConn(t) 51 | vm := commonT.GetVM(t, d, artifacts) 52 | 53 | vmInfo, err := vm.Info("name", "parent", "runtime.host", "resourcePool", "datastore") 54 | if err != nil { 55 | t.Fatalf("Cannot read VM properties: %v", err) 56 | } 57 | 58 | if vmInfo.Name != name { 59 | t.Errorf("Invalid VM name: expected '%v', got '%v'", name, vmInfo.Name) 60 | } 61 | 62 | f := d.NewFolder(vmInfo.Parent) 63 | folderPath, err := f.Path() 64 | if err != nil { 65 | t.Fatalf("Cannot read folder name: %v", err) 66 | } 67 | if folderPath != "" { 68 | t.Errorf("Invalid folder: expected '/', got '%v'", folderPath) 69 | } 70 | 71 | h := d.NewHost(vmInfo.Runtime.Host) 72 | hostInfo, err := h.Info("name") 73 | if err != nil { 74 | t.Fatal("Cannot read host properties: ", err) 75 | } 76 | if hostInfo.Name != host { 77 | t.Errorf("Invalid host name: expected '%v', got '%v'", host, hostInfo.Name) 78 | } 79 | 80 | p := d.NewResourcePool(vmInfo.ResourcePool) 81 | poolPath, err := p.Path() 82 | if err != nil { 83 | t.Fatalf("Cannot read resource pool name: %v", err) 84 | } 85 | if poolPath != "" { 86 | t.Errorf("Invalid resource pool: expected '/', got '%v'", poolPath) 87 | } 88 | 89 | dsr := vmInfo.Datastore[0].Reference() 90 | ds := d.NewDatastore(&dsr) 91 | dsInfo, err := ds.Info("name") 92 | if err != nil { 93 | t.Fatal("Cannot read datastore properties: ", err) 94 | } 95 | if dsInfo.Name != datastore { 96 | t.Errorf("Invalid datastore name: expected '%v', got '%v'", datastore, dsInfo.Name) 97 | } 98 | 99 | return nil 100 | } 101 | } 102 | 103 | func TestCloneBuilderAcc_artifact(t *testing.T) { 104 | config := defaultConfig() 105 | builderT.Test(t, builderT.TestCase{ 106 | Builder: &Builder{}, 107 | Template: commonT.RenderConfig(config), 108 | Check: checkArtifact(t), 109 | }) 110 | } 111 | 112 | func checkArtifact(t *testing.T) builderT.TestCheckFunc { 113 | return func(artifacts []packer.Artifact) error { 114 | if len(artifacts) > 1 { 115 | t.Fatal("more than 1 artifact") 116 | } 117 | 118 | artifactRaw := artifacts[0] 119 | _, ok := artifactRaw.(*common.Artifact) 120 | if !ok { 121 | t.Fatalf("unknown artifact: %#v", artifactRaw) 122 | } 123 | 124 | return nil 125 | } 126 | } 127 | 128 | func TestCloneBuilderAcc_folder(t *testing.T) { 129 | builderT.Test(t, builderT.TestCase{ 130 | Builder: &Builder{}, 131 | Template: folderConfig(), 132 | Check: checkFolder(t, "folder1/folder2"), 133 | }) 134 | } 135 | 136 | func folderConfig() string { 137 | config := defaultConfig() 138 | config["folder"] = "folder1/folder2" 139 | return commonT.RenderConfig(config) 140 | } 141 | 142 | func checkFolder(t *testing.T, folder string) builderT.TestCheckFunc { 143 | return func(artifacts []packer.Artifact) error { 144 | d := commonT.TestConn(t) 145 | vm := commonT.GetVM(t, d, artifacts) 146 | 147 | vmInfo, err := vm.Info("parent") 148 | if err != nil { 149 | t.Fatalf("Cannot read VM properties: %v", err) 150 | } 151 | 152 | f := d.NewFolder(vmInfo.Parent) 153 | path, err := f.Path() 154 | if err != nil { 155 | t.Fatalf("Cannot read folder name: %v", err) 156 | } 157 | if path != folder { 158 | t.Errorf("Wrong folder. expected: %v, got: %v", folder, path) 159 | } 160 | 161 | return nil 162 | } 163 | } 164 | 165 | func TestCloneBuilderAcc_resourcePool(t *testing.T) { 166 | builderT.Test(t, builderT.TestCase{ 167 | Builder: &Builder{}, 168 | Template: resourcePoolConfig(), 169 | Check: checkResourcePool(t, "pool1/pool2"), 170 | }) 171 | } 172 | 173 | func resourcePoolConfig() string { 174 | config := defaultConfig() 175 | config["resource_pool"] = "pool1/pool2" 176 | return commonT.RenderConfig(config) 177 | } 178 | 179 | func checkResourcePool(t *testing.T, pool string) builderT.TestCheckFunc { 180 | return func(artifacts []packer.Artifact) error { 181 | d := commonT.TestConn(t) 182 | vm := commonT.GetVM(t, d, artifacts) 183 | 184 | vmInfo, err := vm.Info("resourcePool") 185 | if err != nil { 186 | t.Fatalf("Cannot read VM properties: %v", err) 187 | } 188 | 189 | p := d.NewResourcePool(vmInfo.ResourcePool) 190 | path, err := p.Path() 191 | if err != nil { 192 | t.Fatalf("Cannot read resource pool name: %v", err) 193 | } 194 | if path != pool { 195 | t.Errorf("Wrong folder. expected: %v, got: %v", pool, path) 196 | } 197 | 198 | return nil 199 | } 200 | } 201 | 202 | func TestCloneBuilderAcc_datastore(t *testing.T) { 203 | builderT.Test(t, builderT.TestCase{ 204 | Builder: &Builder{}, 205 | Template: datastoreConfig(), 206 | Check: checkDatastore(t, "datastore1"), // on esxi-1.vsphere65.test 207 | }) 208 | } 209 | 210 | func datastoreConfig() string { 211 | config := defaultConfig() 212 | config["template"] = "alpine-host4" // on esxi-4.vsphere65.test 213 | config["linked_clone"] = false 214 | return commonT.RenderConfig(config) 215 | } 216 | 217 | func checkDatastore(t *testing.T, name string) builderT.TestCheckFunc { 218 | return func(artifacts []packer.Artifact) error { 219 | d := commonT.TestConn(t) 220 | vm := commonT.GetVM(t, d, artifacts) 221 | 222 | vmInfo, err := vm.Info("datastore") 223 | if err != nil { 224 | t.Fatalf("Cannot read VM properties: %v", err) 225 | } 226 | 227 | n := len(vmInfo.Datastore) 228 | if n != 1 { 229 | t.Fatalf("VM should have 1 datastore, got %v", n) 230 | } 231 | 232 | ds := d.NewDatastore(&vmInfo.Datastore[0]) 233 | info, err := ds.Info("name") 234 | if err != nil { 235 | t.Fatalf("Cannot read datastore properties: %v", err) 236 | } 237 | if info.Name != name { 238 | t.Errorf("Wrong datastore. expected: %v, got: %v", name, info.Name) 239 | } 240 | 241 | return nil 242 | } 243 | } 244 | 245 | func TestCloneBuilderAcc_multipleDatastores(t *testing.T) { 246 | t.Skip("test must fail") 247 | 248 | builderT.Test(t, builderT.TestCase{ 249 | Builder: &Builder{}, 250 | Template: multipleDatastoresConfig(), 251 | }) 252 | } 253 | 254 | func multipleDatastoresConfig() string { 255 | config := defaultConfig() 256 | config["host"] = "esxi-4.vsphere65.test" // host with 2 datastores 257 | config["linked_clone"] = false 258 | return commonT.RenderConfig(config) 259 | } 260 | 261 | func TestCloneBuilderAcc_fullClone(t *testing.T) { 262 | builderT.Test(t, builderT.TestCase{ 263 | Builder: &Builder{}, 264 | Template: fullCloneConfig(), 265 | Check: checkFullClone(t), 266 | }) 267 | } 268 | 269 | func fullCloneConfig() string { 270 | config := defaultConfig() 271 | config["linked_clone"] = false 272 | return commonT.RenderConfig(config) 273 | } 274 | 275 | func checkFullClone(t *testing.T) builderT.TestCheckFunc { 276 | return func(artifacts []packer.Artifact) error { 277 | d := commonT.TestConn(t) 278 | vm := commonT.GetVM(t, d, artifacts) 279 | 280 | vmInfo, err := vm.Info("layoutEx.disk") 281 | if err != nil { 282 | t.Fatalf("Cannot read VM properties: %v", err) 283 | } 284 | 285 | if len(vmInfo.LayoutEx.Disk[0].Chain) != 1 { 286 | t.Error("Not a full clone") 287 | } 288 | 289 | return nil 290 | } 291 | } 292 | 293 | func TestCloneBuilderAcc_linkedClone(t *testing.T) { 294 | builderT.Test(t, builderT.TestCase{ 295 | Builder: &Builder{}, 296 | Template: linkedCloneConfig(), 297 | Check: checkLinkedClone(t), 298 | }) 299 | } 300 | 301 | func linkedCloneConfig() string { 302 | config := defaultConfig() 303 | config["linked_clone"] = true 304 | return commonT.RenderConfig(config) 305 | } 306 | 307 | func checkLinkedClone(t *testing.T) builderT.TestCheckFunc { 308 | return func(artifacts []packer.Artifact) error { 309 | d := commonT.TestConn(t) 310 | vm := commonT.GetVM(t, d, artifacts) 311 | 312 | vmInfo, err := vm.Info("layoutEx.disk") 313 | if err != nil { 314 | t.Fatalf("Cannot read VM properties: %v", err) 315 | } 316 | 317 | if len(vmInfo.LayoutEx.Disk[0].Chain) != 2 { 318 | t.Error("Not a linked clone") 319 | } 320 | 321 | return nil 322 | } 323 | } 324 | 325 | func TestCloneBuilderAcc_network(t *testing.T) { 326 | builderT.Test(t, builderT.TestCase{ 327 | Builder: &Builder{}, 328 | Template: networkConfig(), 329 | Check: checkNetwork(t, "VM Network 2"), 330 | }) 331 | } 332 | 333 | func networkConfig() string { 334 | config := defaultConfig() 335 | config["template"] = "alpine-host4" 336 | config["host"] = "esxi-4.vsphere65.test" 337 | config["datastore"] = "datastore4" 338 | config["network"] = "VM Network 2" 339 | return commonT.RenderConfig(config) 340 | } 341 | 342 | func checkNetwork(t *testing.T, name string) builderT.TestCheckFunc { 343 | return func(artifacts []packer.Artifact) error { 344 | d := commonT.TestConn(t) 345 | vm := commonT.GetVM(t, d, artifacts) 346 | 347 | vmInfo, err := vm.Info("network") 348 | if err != nil { 349 | t.Fatalf("Cannot read VM properties: %v", err) 350 | } 351 | 352 | n := len(vmInfo.Network) 353 | if n != 1 { 354 | t.Fatalf("VM should have 1 network, got %v", n) 355 | } 356 | 357 | ds := d.NewNetwork(&vmInfo.Network[0]) 358 | info, err := ds.Info("name") 359 | if err != nil { 360 | t.Fatalf("Cannot read network properties: %v", err) 361 | } 362 | if info.Name != name { 363 | t.Errorf("Wrong network. expected: %v, got: %v", name, info.Name) 364 | } 365 | 366 | return nil 367 | } 368 | } 369 | 370 | func TestCloneBuilderAcc_hardware(t *testing.T) { 371 | builderT.Test(t, builderT.TestCase{ 372 | Builder: &Builder{}, 373 | Template: hardwareConfig(), 374 | Check: checkHardware(t), 375 | }) 376 | } 377 | 378 | func hardwareConfig() string { 379 | config := defaultConfig() 380 | config["CPUs"] = 2 381 | config["cpu_cores"] = 2 382 | config["CPU_reservation"] = 1000 383 | config["CPU_limit"] = 1500 384 | config["RAM"] = 2048 385 | config["RAM_reservation"] = 1024 386 | config["CPU_hot_plug"] = true 387 | config["RAM_hot_plug"] = true 388 | config["video_ram"] = 8192 389 | 390 | return commonT.RenderConfig(config) 391 | } 392 | 393 | func checkHardware(t *testing.T) builderT.TestCheckFunc { 394 | return func(artifacts []packer.Artifact) error { 395 | d := commonT.TestConn(t) 396 | 397 | vm := commonT.GetVM(t, d, artifacts) 398 | vmInfo, err := vm.Info("config") 399 | if err != nil { 400 | t.Fatalf("Cannot read VM properties: %v", err) 401 | } 402 | 403 | cpuSockets := vmInfo.Config.Hardware.NumCPU 404 | if cpuSockets != 2 { 405 | t.Errorf("VM should have 2 CPU sockets, got %v", cpuSockets) 406 | } 407 | 408 | cpuCores := vmInfo.Config.Hardware.NumCoresPerSocket 409 | if cpuCores != 2 { 410 | t.Errorf("VM should have 2 CPU cores per socket, got %v", cpuCores) 411 | } 412 | 413 | cpuReservation := *vmInfo.Config.CpuAllocation.Reservation 414 | if cpuReservation != 1000 { 415 | t.Errorf("VM should have CPU reservation for 1000 Mhz, got %v", cpuReservation) 416 | } 417 | 418 | cpuLimit := *vmInfo.Config.CpuAllocation.Limit 419 | if cpuLimit != 1500 { 420 | t.Errorf("VM should have CPU reservation for 1500 Mhz, got %v", cpuLimit) 421 | } 422 | 423 | ram := vmInfo.Config.Hardware.MemoryMB 424 | if ram != 2048 { 425 | t.Errorf("VM should have 2048 MB of RAM, got %v", ram) 426 | } 427 | 428 | ramReservation := *vmInfo.Config.MemoryAllocation.Reservation 429 | if ramReservation != 1024 { 430 | t.Errorf("VM should have RAM reservation for 1024 MB, got %v", ramReservation) 431 | } 432 | 433 | cpuHotAdd := vmInfo.Config.CpuHotAddEnabled 434 | if !*cpuHotAdd { 435 | t.Errorf("VM should have CPU hot add enabled, got %v", cpuHotAdd) 436 | } 437 | 438 | memoryHotAdd := vmInfo.Config.MemoryHotAddEnabled 439 | if !*memoryHotAdd { 440 | t.Errorf("VM should have Memory hot add enabled, got %v", memoryHotAdd) 441 | } 442 | 443 | l, err := vm.Devices() 444 | if err != nil { 445 | t.Fatalf("Cannot read VM devices: %v", err) 446 | } 447 | v := l.SelectByType((*types.VirtualMachineVideoCard)(nil)) 448 | if len(v) != 1 { 449 | t.Errorf("VM should have one video card") 450 | } 451 | if v[0].(*types.VirtualMachineVideoCard).VideoRamSizeInKB != 8192 { 452 | t.Errorf("Video RAM should be equal 8192") 453 | } 454 | 455 | return nil 456 | } 457 | } 458 | 459 | func TestCloneBuilderAcc_RAMReservation(t *testing.T) { 460 | builderT.Test(t, builderT.TestCase{ 461 | Builder: &Builder{}, 462 | Template: RAMReservationConfig(), 463 | Check: checkRAMReservation(t), 464 | }) 465 | } 466 | 467 | func RAMReservationConfig() string { 468 | config := defaultConfig() 469 | config["RAM_reserve_all"] = true 470 | 471 | return commonT.RenderConfig(config) 472 | } 473 | 474 | func checkRAMReservation(t *testing.T) builderT.TestCheckFunc { 475 | return func(artifacts []packer.Artifact) error { 476 | d := commonT.TestConn(t) 477 | 478 | vm := commonT.GetVM(t, d, artifacts) 479 | vmInfo, err := vm.Info("config") 480 | if err != nil { 481 | t.Fatalf("Cannot read VM properties: %v", err) 482 | } 483 | 484 | if *vmInfo.Config.MemoryReservationLockedToMax != true { 485 | t.Errorf("VM should have all RAM reserved") 486 | } 487 | 488 | return nil 489 | } 490 | } 491 | 492 | func TestCloneBuilderAcc_sshPassword(t *testing.T) { 493 | builderT.Test(t, builderT.TestCase{ 494 | Builder: &Builder{}, 495 | Template: sshPasswordConfig(), 496 | Check: checkDefaultBootOrder(t), 497 | }) 498 | } 499 | 500 | func sshPasswordConfig() string { 501 | config := defaultConfig() 502 | config["communicator"] = "ssh" 503 | config["ssh_username"] = "root" 504 | config["ssh_password"] = "jetbrains" 505 | return commonT.RenderConfig(config) 506 | } 507 | 508 | func checkDefaultBootOrder(t *testing.T) builderT.TestCheckFunc { 509 | return func(artifacts []packer.Artifact) error { 510 | d := commonT.TestConn(t) 511 | vm := commonT.GetVM(t, d, artifacts) 512 | 513 | vmInfo, err := vm.Info("config.bootOptions") 514 | if err != nil { 515 | t.Fatalf("Cannot read VM properties: %v", err) 516 | } 517 | 518 | order := vmInfo.Config.BootOptions.BootOrder 519 | if order != nil { 520 | t.Errorf("Boot order must be empty") 521 | } 522 | 523 | return nil 524 | } 525 | } 526 | 527 | func TestCloneBuilderAcc_sshKey(t *testing.T) { 528 | builderT.Test(t, builderT.TestCase{ 529 | Builder: &Builder{}, 530 | Template: sshKeyConfig(), 531 | }) 532 | } 533 | 534 | func sshKeyConfig() string { 535 | config := defaultConfig() 536 | config["communicator"] = "ssh" 537 | config["ssh_username"] = "root" 538 | config["ssh_private_key_file"] = "../test/test-key.pem" 539 | return commonT.RenderConfig(config) 540 | } 541 | 542 | func TestCloneBuilderAcc_snapshot(t *testing.T) { 543 | builderT.Test(t, builderT.TestCase{ 544 | Builder: &Builder{}, 545 | Template: snapshotConfig(), 546 | Check: checkSnapshot(t), 547 | }) 548 | } 549 | 550 | func snapshotConfig() string { 551 | config := defaultConfig() 552 | config["linked_clone"] = false 553 | config["create_snapshot"] = true 554 | return commonT.RenderConfig(config) 555 | } 556 | 557 | func checkSnapshot(t *testing.T) builderT.TestCheckFunc { 558 | return func(artifacts []packer.Artifact) error { 559 | d := commonT.TestConn(t) 560 | 561 | vm := commonT.GetVM(t, d, artifacts) 562 | vmInfo, err := vm.Info("layoutEx.disk") 563 | if err != nil { 564 | t.Fatalf("Cannot read VM properties: %v", err) 565 | } 566 | 567 | layers := len(vmInfo.LayoutEx.Disk[0].Chain) 568 | if layers != 2 { 569 | t.Errorf("VM should have a single snapshot. expected 2 disk layers, got %v", layers) 570 | } 571 | 572 | return nil 573 | } 574 | } 575 | 576 | func TestCloneBuilderAcc_template(t *testing.T) { 577 | builderT.Test(t, builderT.TestCase{ 578 | Builder: &Builder{}, 579 | Template: templateConfig(), 580 | Check: checkTemplate(t), 581 | }) 582 | } 583 | 584 | func templateConfig() string { 585 | config := defaultConfig() 586 | config["convert_to_template"] = true 587 | return commonT.RenderConfig(config) 588 | } 589 | 590 | func checkTemplate(t *testing.T) builderT.TestCheckFunc { 591 | return func(artifacts []packer.Artifact) error { 592 | d := commonT.TestConn(t) 593 | 594 | vm := commonT.GetVM(t, d, artifacts) 595 | vmInfo, err := vm.Info("config.template") 596 | if err != nil { 597 | t.Fatalf("Cannot read VM properties: %v", err) 598 | } 599 | 600 | if vmInfo.Config.Template != true { 601 | t.Error("Not a template") 602 | } 603 | 604 | return nil 605 | } 606 | } 607 | 608 | func TestCloneBuilderAcc_bootOrder(t *testing.T) { 609 | builderT.Test(t, builderT.TestCase{ 610 | Builder: &Builder{}, 611 | Template: bootOrderConfig(), 612 | Check: checkBootOrder(t), 613 | }) 614 | } 615 | 616 | func bootOrderConfig() string { 617 | config := defaultConfig() 618 | config["communicator"] = "ssh" 619 | config["ssh_username"] = "root" 620 | config["ssh_password"] = "jetbrains" 621 | 622 | config["boot_order"] = "disk,cdrom,floppy" 623 | 624 | return commonT.RenderConfig(config) 625 | } 626 | 627 | func checkBootOrder(t *testing.T) builderT.TestCheckFunc { 628 | return func(artifacts []packer.Artifact) error { 629 | d := commonT.TestConn(t) 630 | vm := commonT.GetVM(t, d, artifacts) 631 | 632 | vmInfo, err := vm.Info("config.bootOptions") 633 | if err != nil { 634 | t.Fatalf("Cannot read VM properties: %v", err) 635 | } 636 | 637 | order := vmInfo.Config.BootOptions.BootOrder 638 | if order == nil { 639 | t.Errorf("Boot order must not be empty") 640 | } 641 | 642 | return nil 643 | } 644 | } 645 | 646 | func TestCloneBuilderAcc_notes(t *testing.T) { 647 | builderT.Test(t, builderT.TestCase{ 648 | Builder: &Builder{}, 649 | Template: notesConfig(), 650 | Check: checkNotes(t), 651 | }) 652 | } 653 | 654 | func notesConfig() string { 655 | config := defaultConfig() 656 | config["notes"] = "test" 657 | 658 | return commonT.RenderConfig(config) 659 | } 660 | 661 | func checkNotes(t *testing.T) builderT.TestCheckFunc { 662 | return func(artifacts []packer.Artifact) error { 663 | d := commonT.TestConn(t) 664 | vm := commonT.GetVM(t, d, artifacts) 665 | 666 | vmInfo, err := vm.Info("config.annotation") 667 | if err != nil { 668 | t.Fatalf("Cannot read VM properties: %v", err) 669 | } 670 | 671 | notes := vmInfo.Config.Annotation 672 | if notes != "test" { 673 | t.Errorf("notest should be 'test'") 674 | } 675 | 676 | return nil 677 | } 678 | } 679 | 680 | func TestCloneBuilderAcc_windows(t *testing.T) { 681 | t.Skip("test is too slow") 682 | config := windowsConfig() 683 | builderT.Test(t, builderT.TestCase{ 684 | Builder: &Builder{}, 685 | Template: commonT.RenderConfig(config), 686 | }) 687 | } 688 | 689 | func windowsConfig() map[string]interface{} { 690 | username := os.Getenv("VSPHERE_USERNAME") 691 | if username == "" { 692 | username = "root" 693 | } 694 | password := os.Getenv("VSPHERE_PASSWORD") 695 | if password == "" { 696 | password = "jetbrains" 697 | } 698 | 699 | config := map[string]interface{}{ 700 | "vcenter_server": "vcenter.vsphere65.test", 701 | "username": username, 702 | "password": password, 703 | "insecure_connection": true, 704 | 705 | "vm_name": commonT.NewVMName(), 706 | "template": "windows", 707 | "host": "esxi-1.vsphere65.test", 708 | "linked_clone": true, // speed up 709 | 710 | "communicator": "winrm", 711 | "winrm_username": "jetbrains", 712 | "winrm_password": "jetbrains", 713 | } 714 | 715 | return config 716 | } 717 | -------------------------------------------------------------------------------- /clone/builder_test.go: -------------------------------------------------------------------------------- 1 | package clone 2 | 3 | import ( 4 | "github.com/hashicorp/packer/packer" 5 | "testing" 6 | ) 7 | 8 | func TestCloneBuilder_ImplementsBuilder(t *testing.T) { 9 | var raw interface{} 10 | raw = &Builder{} 11 | if _, ok := raw.(packer.Builder); !ok { 12 | t.Fatalf("Builder should be a builder") 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /clone/config.go: -------------------------------------------------------------------------------- 1 | package clone 2 | 3 | import ( 4 | packerCommon "github.com/hashicorp/packer/common" 5 | "github.com/hashicorp/packer/helper/communicator" 6 | "github.com/hashicorp/packer/helper/config" 7 | "github.com/hashicorp/packer/packer" 8 | "github.com/hashicorp/packer/template/interpolate" 9 | "github.com/jetbrains-infra/packer-builder-vsphere/common" 10 | ) 11 | 12 | type Config struct { 13 | packerCommon.PackerConfig `mapstructure:",squash"` 14 | 15 | common.ConnectConfig `mapstructure:",squash"` 16 | CloneConfig `mapstructure:",squash"` 17 | common.LocationConfig `mapstructure:",squash"` 18 | common.HardwareConfig `mapstructure:",squash"` 19 | common.ConfigParamsConfig `mapstructure:",squash"` 20 | 21 | common.RunConfig `mapstructure:",squash"` 22 | common.WaitIpConfig `mapstructure:",squash"` 23 | Comm communicator.Config `mapstructure:",squash"` 24 | common.ShutdownConfig `mapstructure:",squash"` 25 | 26 | CreateSnapshot bool `mapstructure:"create_snapshot"` 27 | ConvertToTemplate bool `mapstructure:"convert_to_template"` 28 | 29 | ctx interpolate.Context 30 | } 31 | 32 | func NewConfig(raws ...interface{}) (*Config, []string, error) { 33 | c := new(Config) 34 | err := config.Decode(c, &config.DecodeOpts{ 35 | Interpolate: true, 36 | InterpolateContext: &c.ctx, 37 | }, raws...) 38 | if err != nil { 39 | return nil, nil, err 40 | } 41 | 42 | errs := new(packer.MultiError) 43 | errs = packer.MultiErrorAppend(errs, c.ConnectConfig.Prepare()...) 44 | errs = packer.MultiErrorAppend(errs, c.CloneConfig.Prepare()...) 45 | errs = packer.MultiErrorAppend(errs, c.LocationConfig.Prepare()...) 46 | errs = packer.MultiErrorAppend(errs, c.HardwareConfig.Prepare()...) 47 | 48 | errs = packer.MultiErrorAppend(errs, c.WaitIpConfig.Prepare()...) 49 | errs = packer.MultiErrorAppend(errs, c.Comm.Prepare(&c.ctx)...) 50 | errs = packer.MultiErrorAppend(errs, c.ShutdownConfig.Prepare()...) 51 | 52 | if len(errs.Errors) > 0 { 53 | return nil, nil, errs 54 | } 55 | 56 | return c, nil, nil 57 | } 58 | -------------------------------------------------------------------------------- /clone/config_test.go: -------------------------------------------------------------------------------- 1 | package clone 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestCloneConfig_MinimalConfig(t *testing.T) { 9 | _, warns, errs := NewConfig(minimalConfig()) 10 | testConfigOk(t, warns, errs) 11 | } 12 | 13 | func TestCloneConfig_MandatoryParameters(t *testing.T) { 14 | params := []string{"vcenter_server", "username", "password", "template", "vm_name", "host"} 15 | for _, param := range params { 16 | raw := minimalConfig() 17 | raw[param] = "" 18 | _, warns, err := NewConfig(raw) 19 | testConfigErr(t, param, warns, err) 20 | } 21 | } 22 | 23 | func TestCloneConfig_Timeout(t *testing.T) { 24 | raw := minimalConfig() 25 | raw["shutdown_timeout"] = "3m" 26 | conf, warns, err := NewConfig(raw) 27 | testConfigOk(t, warns, err) 28 | if conf.ShutdownConfig.Timeout != 3*time.Minute { 29 | t.Fatalf("shutdown_timeout sould be equal 3 minutes, got %v", conf.ShutdownConfig.Timeout) 30 | } 31 | } 32 | 33 | func TestCloneConfig_RAMReservation(t *testing.T) { 34 | raw := minimalConfig() 35 | raw["RAM_reservation"] = 1000 36 | raw["RAM_reserve_all"] = true 37 | _, warns, err := NewConfig(raw) 38 | testConfigErr(t, "RAM_reservation", warns, err) 39 | } 40 | 41 | func minimalConfig() map[string]interface{} { 42 | return map[string]interface{}{ 43 | "vcenter_server": "vcenter.domain.local", 44 | "username": "root", 45 | "password": "vmware", 46 | "template": "ubuntu", 47 | "vm_name": "vm1", 48 | "host": "esxi1.domain.local", 49 | "ssh_username": "root", 50 | "ssh_password": "secret", 51 | } 52 | } 53 | 54 | func testConfigOk(t *testing.T, warns []string, err error) { 55 | if len(warns) > 0 { 56 | t.Errorf("Should be no warnings: %#v", warns) 57 | } 58 | if err != nil { 59 | t.Errorf("Unexpected error: %s", err) 60 | } 61 | } 62 | 63 | func testConfigErr(t *testing.T, context string, warns []string, err error) { 64 | if len(warns) > 0 { 65 | t.Errorf("Should be no warnings: %#v", warns) 66 | } 67 | if err == nil { 68 | t.Error("An error is not raised for", context) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /clone/leak_test.go: -------------------------------------------------------------------------------- 1 | package clone 2 | 3 | import "testing" 4 | import "go.uber.org/goleak" 5 | 6 | func TestMain(m *testing.M) { 7 | goleak.VerifyTestMain(m, goleak.IgnoreTopFunction("go.opencensus.io/stats/view.(*worker).start")) 8 | } 9 | -------------------------------------------------------------------------------- /clone/step_clone.go: -------------------------------------------------------------------------------- 1 | package clone 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/hashicorp/packer/helper/multistep" 7 | "github.com/hashicorp/packer/packer" 8 | "github.com/jetbrains-infra/packer-builder-vsphere/common" 9 | "github.com/jetbrains-infra/packer-builder-vsphere/driver" 10 | ) 11 | 12 | type CloneConfig struct { 13 | Template string `mapstructure:"template"` 14 | DiskSize int64 `mapstructure:"disk_size"` 15 | LinkedClone bool `mapstructure:"linked_clone"` 16 | Network string `mapstructure:"network"` 17 | Notes string `mapstructure:"notes"` 18 | } 19 | 20 | func (c *CloneConfig) Prepare() []error { 21 | var errs []error 22 | 23 | if c.Template == "" { 24 | errs = append(errs, fmt.Errorf("'template' is required")) 25 | } 26 | 27 | if c.LinkedClone == true && c.DiskSize != 0 { 28 | errs = append(errs, fmt.Errorf("'linked_clone' and 'disk_size' cannot be used together")) 29 | } 30 | 31 | return errs 32 | } 33 | 34 | type StepCloneVM struct { 35 | Config *CloneConfig 36 | Location *common.LocationConfig 37 | Force bool 38 | } 39 | 40 | func (s *StepCloneVM) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { 41 | ui := state.Get("ui").(packer.Ui) 42 | d := state.Get("driver").(*driver.Driver) 43 | 44 | vm, err := d.FindVM(s.Location.VMName) 45 | 46 | if s.Force == false && err == nil { 47 | state.Put("error", fmt.Errorf("%s already exists, you can use -force flag to destroy it", s.Location.VMName)) 48 | return multistep.ActionHalt 49 | } else if s.Force == true && err == nil { 50 | ui.Say(fmt.Sprintf("the vm/template %s already exists, but deleting it due to -force flag", s.Location.VMName)) 51 | err := vm.Destroy() 52 | if err != nil { 53 | state.Put("error", fmt.Errorf("error destroying %s: %v", s.Location.VMName, err)) 54 | } 55 | } 56 | 57 | ui.Say("Cloning VM...") 58 | template, err := d.FindVM(s.Config.Template) 59 | if err != nil { 60 | state.Put("error", err) 61 | return multistep.ActionHalt 62 | } 63 | 64 | vm, err = template.Clone(ctx, &driver.CloneConfig{ 65 | Name: s.Location.VMName, 66 | Folder: s.Location.Folder, 67 | Cluster: s.Location.Cluster, 68 | Host: s.Location.Host, 69 | ResourcePool: s.Location.ResourcePool, 70 | Datastore: s.Location.Datastore, 71 | LinkedClone: s.Config.LinkedClone, 72 | Network: s.Config.Network, 73 | Annotation: s.Config.Notes, 74 | }) 75 | if err != nil { 76 | state.Put("error", err) 77 | return multistep.ActionHalt 78 | } 79 | if vm == nil { 80 | return multistep.ActionHalt 81 | } 82 | state.Put("vm", vm) 83 | 84 | if s.Config.DiskSize > 0 { 85 | err = vm.ResizeDisk(s.Config.DiskSize) 86 | if err != nil { 87 | state.Put("error", err) 88 | return multistep.ActionHalt 89 | } 90 | } 91 | 92 | return multistep.ActionContinue 93 | } 94 | 95 | func (s *StepCloneVM) Cleanup(state multistep.StateBag) { 96 | _, cancelled := state.GetOk(multistep.StateCancelled) 97 | _, halted := state.GetOk(multistep.StateHalted) 98 | if !cancelled && !halted { 99 | return 100 | } 101 | 102 | ui := state.Get("ui").(packer.Ui) 103 | 104 | st := state.Get("vm") 105 | if st == nil { 106 | return 107 | } 108 | vm := st.(*driver.VirtualMachine) 109 | 110 | ui.Say("Destroying VM...") 111 | 112 | err := vm.Destroy() 113 | if err != nil { 114 | ui.Error(err.Error()) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /cmd/clone/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/hashicorp/packer/packer/plugin" 4 | import "github.com/jetbrains-infra/packer-builder-vsphere/clone" 5 | 6 | func main() { 7 | server, err := plugin.Server() 8 | if err != nil { 9 | panic(err) 10 | } 11 | err = server.RegisterBuilder(new(clone.Builder)) 12 | if err != nil { 13 | panic(err) 14 | } 15 | server.Serve() 16 | } 17 | -------------------------------------------------------------------------------- /cmd/iso/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/hashicorp/packer/packer/plugin" 4 | import "github.com/jetbrains-infra/packer-builder-vsphere/iso" 5 | 6 | func main() { 7 | server, err := plugin.Server() 8 | if err != nil { 9 | panic(err) 10 | } 11 | err = server.RegisterBuilder(new(iso.Builder)) 12 | if err != nil { 13 | panic(err) 14 | } 15 | server.Serve() 16 | } 17 | -------------------------------------------------------------------------------- /common/artifact.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "github.com/jetbrains-infra/packer-builder-vsphere/driver" 5 | ) 6 | 7 | const BuilderId = "jetbrains.vsphere" 8 | 9 | type Artifact struct { 10 | Name string 11 | VM *driver.VirtualMachine 12 | } 13 | 14 | func (a *Artifact) BuilderId() string { 15 | return BuilderId 16 | } 17 | 18 | func (a *Artifact) Files() []string { 19 | return []string{} 20 | } 21 | 22 | func (a *Artifact) Id() string { 23 | return a.Name 24 | } 25 | 26 | func (a *Artifact) String() string { 27 | return a.Name 28 | } 29 | 30 | func (a *Artifact) State(name string) interface{} { 31 | return nil 32 | } 33 | 34 | func (a *Artifact) Destroy() error { 35 | return a.VM.Destroy() 36 | } 37 | -------------------------------------------------------------------------------- /common/config_location.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import "fmt" 4 | 5 | type LocationConfig struct { 6 | VMName string `mapstructure:"vm_name"` 7 | Folder string `mapstructure:"folder"` 8 | Cluster string `mapstructure:"cluster"` 9 | Host string `mapstructure:"host"` 10 | ResourcePool string `mapstructure:"resource_pool"` 11 | Datastore string `mapstructure:"datastore"` 12 | } 13 | 14 | func (c *LocationConfig) Prepare() []error { 15 | var errs []error 16 | 17 | if c.VMName == "" { 18 | errs = append(errs, fmt.Errorf("'vm_name' is required")) 19 | } 20 | if c.Cluster == "" && c.Host == "" { 21 | errs = append(errs, fmt.Errorf("'host' or 'cluster' is required")) 22 | } 23 | 24 | return errs 25 | } 26 | -------------------------------------------------------------------------------- /common/config_ssh.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "github.com/hashicorp/packer/helper/multistep" 5 | ) 6 | 7 | func CommHost(host string) func(multistep.StateBag) (string, error) { 8 | return func(state multistep.StateBag) (string, error) { 9 | if host != "" { 10 | return host, nil 11 | } else { 12 | return state.Get("ip").(string), nil 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /common/step_config_params.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/hashicorp/packer/helper/multistep" 7 | "github.com/hashicorp/packer/packer" 8 | "github.com/jetbrains-infra/packer-builder-vsphere/driver" 9 | ) 10 | 11 | type ConfigParamsConfig struct { 12 | ConfigParams map[string]string `mapstructure:"configuration_parameters"` 13 | } 14 | 15 | type StepConfigParams struct { 16 | Config *ConfigParamsConfig 17 | } 18 | 19 | func (s *StepConfigParams) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { 20 | ui := state.Get("ui").(packer.Ui) 21 | vm := state.Get("vm").(*driver.VirtualMachine) 22 | 23 | if s.Config.ConfigParams != nil { 24 | ui.Say("Adding configuration parameters...") 25 | if err := vm.AddConfigParams(s.Config.ConfigParams); err != nil { 26 | state.Put("error", fmt.Errorf("error adding configuration parameters: %v", err)) 27 | return multistep.ActionHalt 28 | } 29 | } 30 | 31 | return multistep.ActionContinue 32 | } 33 | 34 | func (s *StepConfigParams) Cleanup(state multistep.StateBag) {} 35 | -------------------------------------------------------------------------------- /common/step_connect.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/hashicorp/packer/helper/multistep" 7 | "github.com/jetbrains-infra/packer-builder-vsphere/driver" 8 | ) 9 | 10 | type ConnectConfig struct { 11 | VCenterServer string `mapstructure:"vcenter_server"` 12 | Username string `mapstructure:"username"` 13 | Password string `mapstructure:"password"` 14 | InsecureConnection bool `mapstructure:"insecure_connection"` 15 | Datacenter string `mapstructure:"datacenter"` 16 | } 17 | 18 | func (c *ConnectConfig) Prepare() []error { 19 | var errs []error 20 | 21 | if c.VCenterServer == "" { 22 | errs = append(errs, fmt.Errorf("'vcenter_server' is required")) 23 | } 24 | if c.Username == "" { 25 | errs = append(errs, fmt.Errorf("'username' is required")) 26 | } 27 | if c.Password == "" { 28 | errs = append(errs, fmt.Errorf("'password' is required")) 29 | } 30 | 31 | return errs 32 | } 33 | 34 | type StepConnect struct { 35 | Config *ConnectConfig 36 | } 37 | 38 | func (s *StepConnect) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { 39 | d, err := driver.NewDriver(&driver.ConnectConfig{ 40 | VCenterServer: s.Config.VCenterServer, 41 | Username: s.Config.Username, 42 | Password: s.Config.Password, 43 | InsecureConnection: s.Config.InsecureConnection, 44 | Datacenter: s.Config.Datacenter, 45 | }) 46 | if err != nil { 47 | state.Put("error", err) 48 | return multistep.ActionHalt 49 | } 50 | state.Put("driver", d) 51 | 52 | return multistep.ActionContinue 53 | } 54 | 55 | func (s *StepConnect) Cleanup(multistep.StateBag) {} 56 | -------------------------------------------------------------------------------- /common/step_hardware.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/hashicorp/packer/helper/multistep" 7 | "github.com/hashicorp/packer/packer" 8 | "github.com/jetbrains-infra/packer-builder-vsphere/driver" 9 | ) 10 | 11 | type HardwareConfig struct { 12 | CPUs int32 `mapstructure:"CPUs"` 13 | CpuCores int32 `mapstructure:"cpu_cores"` 14 | CPUReservation int64 `mapstructure:"CPU_reservation"` 15 | CPULimit int64 `mapstructure:"CPU_limit"` 16 | CpuHotAddEnabled bool `mapstructure:"CPU_hot_plug"` 17 | 18 | RAM int64 `mapstructure:"RAM"` 19 | RAMReservation int64 `mapstructure:"RAM_reservation"` 20 | RAMReserveAll bool `mapstructure:"RAM_reserve_all"` 21 | MemoryHotAddEnabled bool `mapstructure:"RAM_hot_plug"` 22 | 23 | VideoRAM int64 `mapstructure:"video_ram"` 24 | NestedHV bool `mapstructure:"NestedHV"` 25 | } 26 | 27 | func (c *HardwareConfig) Prepare() []error { 28 | var errs []error 29 | 30 | if c.RAMReservation > 0 && c.RAMReserveAll != false { 31 | errs = append(errs, fmt.Errorf("'RAM_reservation' and 'RAM_reserve_all' cannot be used together")) 32 | } 33 | 34 | return errs 35 | } 36 | 37 | type StepConfigureHardware struct { 38 | Config *HardwareConfig 39 | } 40 | 41 | func (s *StepConfigureHardware) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { 42 | ui := state.Get("ui").(packer.Ui) 43 | vm := state.Get("vm").(*driver.VirtualMachine) 44 | 45 | if *s.Config != (HardwareConfig{}) { 46 | ui.Say("Customizing hardware...") 47 | 48 | err := vm.Configure(&driver.HardwareConfig{ 49 | CPUs: s.Config.CPUs, 50 | CpuCores: s.Config.CpuCores, 51 | CPUReservation: s.Config.CPUReservation, 52 | CPULimit: s.Config.CPULimit, 53 | RAM: s.Config.RAM, 54 | RAMReservation: s.Config.RAMReservation, 55 | RAMReserveAll: s.Config.RAMReserveAll, 56 | NestedHV: s.Config.NestedHV, 57 | CpuHotAddEnabled: s.Config.CpuHotAddEnabled, 58 | MemoryHotAddEnabled: s.Config.MemoryHotAddEnabled, 59 | VideoRAM: s.Config.VideoRAM, 60 | }) 61 | if err != nil { 62 | state.Put("error", err) 63 | return multistep.ActionHalt 64 | } 65 | } 66 | 67 | return multistep.ActionContinue 68 | } 69 | 70 | func (s *StepConfigureHardware) Cleanup(multistep.StateBag) {} 71 | -------------------------------------------------------------------------------- /common/step_run.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "context" 5 | "github.com/hashicorp/packer/helper/multistep" 6 | "github.com/hashicorp/packer/packer" 7 | "github.com/jetbrains-infra/packer-builder-vsphere/driver" 8 | "strings" 9 | ) 10 | 11 | type RunConfig struct { 12 | BootOrder string `mapstructure:"boot_order"` // example: "floppy,cdrom,ethernet,disk" 13 | } 14 | 15 | type StepRun struct { 16 | Config *RunConfig 17 | SetOrder bool 18 | } 19 | 20 | func (s *StepRun) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { 21 | ui := state.Get("ui").(packer.Ui) 22 | vm := state.Get("vm").(*driver.VirtualMachine) 23 | 24 | if s.Config.BootOrder != "" { 25 | ui.Say("Set boot order...") 26 | order := strings.Split(s.Config.BootOrder, ",") 27 | if err := vm.SetBootOrder(order); err != nil { 28 | state.Put("error", err) 29 | return multistep.ActionHalt 30 | } 31 | } else { 32 | if s.SetOrder { 33 | ui.Say("Set boot order temporary...") 34 | if err := vm.SetBootOrder([]string{"disk", "cdrom"}); err != nil { 35 | state.Put("error", err) 36 | return multistep.ActionHalt 37 | } 38 | } 39 | } 40 | 41 | ui.Say("Power on VM...") 42 | err := vm.PowerOn() 43 | if err != nil { 44 | state.Put("error", err) 45 | return multistep.ActionHalt 46 | } 47 | 48 | return multistep.ActionContinue 49 | } 50 | 51 | func (s *StepRun) Cleanup(state multistep.StateBag) { 52 | ui := state.Get("ui").(packer.Ui) 53 | vm := state.Get("vm").(*driver.VirtualMachine) 54 | 55 | if s.Config.BootOrder == "" && s.SetOrder { 56 | ui.Say("Clear boot order...") 57 | if err := vm.SetBootOrder([]string{"-"}); err != nil { 58 | state.Put("error", err) 59 | return 60 | } 61 | } 62 | 63 | _, cancelled := state.GetOk(multistep.StateCancelled) 64 | _, halted := state.GetOk(multistep.StateHalted) 65 | if !cancelled && !halted { 66 | return 67 | } 68 | 69 | ui.Say("Power off VM...") 70 | 71 | err := vm.PowerOff() 72 | if err != nil { 73 | ui.Error(err.Error()) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /common/step_shutdown.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "github.com/hashicorp/packer/helper/multistep" 8 | "github.com/hashicorp/packer/packer" 9 | "github.com/jetbrains-infra/packer-builder-vsphere/driver" 10 | "log" 11 | "time" 12 | ) 13 | 14 | type ShutdownConfig struct { 15 | Command string `mapstructure:"shutdown_command"` 16 | Timeout time.Duration `mapstructure:"shutdown_timeout"` 17 | } 18 | 19 | func (c *ShutdownConfig) Prepare() []error { 20 | var errs []error 21 | 22 | if c.Timeout == 0 { 23 | c.Timeout = 5 * time.Minute 24 | } 25 | 26 | return errs 27 | } 28 | 29 | type StepShutdown struct { 30 | Config *ShutdownConfig 31 | } 32 | 33 | func (s *StepShutdown) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { 34 | ui := state.Get("ui").(packer.Ui) 35 | comm := state.Get("communicator").(packer.Communicator) 36 | vm := state.Get("vm").(*driver.VirtualMachine) 37 | 38 | if s.Config.Command != "" { 39 | ui.Say("Executing shutdown command...") 40 | log.Printf("Shutdown command: %s", s.Config.Command) 41 | 42 | var stdout, stderr bytes.Buffer 43 | cmd := &packer.RemoteCmd{ 44 | Command: s.Config.Command, 45 | Stdout: &stdout, 46 | Stderr: &stderr, 47 | } 48 | err := comm.Start(ctx, cmd) 49 | if err != nil { 50 | state.Put("error", fmt.Errorf("Failed to send shutdown command: %s", err)) 51 | return multistep.ActionHalt 52 | } 53 | } else { 54 | ui.Say("Shut down VM...") 55 | 56 | err := vm.StartShutdown() 57 | if err != nil { 58 | state.Put("error", fmt.Errorf("Cannot shut down VM: %v", err)) 59 | return multistep.ActionHalt 60 | } 61 | } 62 | 63 | log.Printf("Waiting max %s for shutdown to complete", s.Config.Timeout) 64 | err := vm.WaitForShutdown(ctx, s.Config.Timeout) 65 | if err != nil { 66 | state.Put("error", err) 67 | return multistep.ActionHalt 68 | } 69 | 70 | return multistep.ActionContinue 71 | } 72 | 73 | func (s *StepShutdown) Cleanup(state multistep.StateBag) {} 74 | -------------------------------------------------------------------------------- /common/step_snapshot.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "context" 5 | "github.com/hashicorp/packer/helper/multistep" 6 | "github.com/hashicorp/packer/packer" 7 | "github.com/jetbrains-infra/packer-builder-vsphere/driver" 8 | ) 9 | 10 | type StepCreateSnapshot struct { 11 | CreateSnapshot bool 12 | } 13 | 14 | func (s *StepCreateSnapshot) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { 15 | ui := state.Get("ui").(packer.Ui) 16 | vm := state.Get("vm").(*driver.VirtualMachine) 17 | 18 | if s.CreateSnapshot { 19 | ui.Say("Creating snapshot...") 20 | 21 | err := vm.CreateSnapshot("Created by Packer") 22 | if err != nil { 23 | state.Put("error", err) 24 | return multistep.ActionHalt 25 | } 26 | } 27 | 28 | return multistep.ActionContinue 29 | } 30 | 31 | func (s *StepCreateSnapshot) Cleanup(state multistep.StateBag) {} 32 | -------------------------------------------------------------------------------- /common/step_template.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "context" 5 | "github.com/hashicorp/packer/helper/multistep" 6 | "github.com/hashicorp/packer/packer" 7 | "github.com/jetbrains-infra/packer-builder-vsphere/driver" 8 | ) 9 | 10 | type StepConvertToTemplate struct { 11 | ConvertToTemplate bool 12 | } 13 | 14 | func (s *StepConvertToTemplate) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { 15 | ui := state.Get("ui").(packer.Ui) 16 | vm := state.Get("vm").(*driver.VirtualMachine) 17 | 18 | if s.ConvertToTemplate { 19 | ui.Say("Convert VM into template...") 20 | err := vm.ConvertToTemplate() 21 | if err != nil { 22 | state.Put("error", err) 23 | return multistep.ActionHalt 24 | } 25 | } 26 | 27 | return multistep.ActionContinue 28 | } 29 | 30 | func (s *StepConvertToTemplate) Cleanup(state multistep.StateBag) {} 31 | -------------------------------------------------------------------------------- /common/step_wait_for_ip.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/hashicorp/packer/helper/multistep" 7 | "github.com/hashicorp/packer/packer" 8 | "github.com/jetbrains-infra/packer-builder-vsphere/driver" 9 | "log" 10 | "time" 11 | ) 12 | 13 | type WaitIpConfig struct { 14 | WaitTimeout time.Duration `mapstructure:"ip_wait_timeout"` 15 | SettleTimeout time.Duration `mapstructure:"ip_settle_timeout"` 16 | 17 | // WaitTimeout is a total timeout, so even if VM changes IP frequently and it doesn't settle down we will end waiting. 18 | } 19 | 20 | type StepWaitForIp struct { 21 | Config *WaitIpConfig 22 | } 23 | 24 | func (c *WaitIpConfig) Prepare() []error { 25 | var errs []error 26 | 27 | if c.SettleTimeout == 0 { 28 | c.SettleTimeout = 5 * time.Second 29 | } 30 | if c.WaitTimeout == 0 { 31 | c.WaitTimeout = 30 * time.Minute 32 | } 33 | 34 | return errs 35 | } 36 | 37 | func (s *StepWaitForIp) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { 38 | ui := state.Get("ui").(packer.Ui) 39 | vm := state.Get("vm").(*driver.VirtualMachine) 40 | 41 | var ip string 42 | var err error 43 | 44 | sub, cancel := context.WithCancel(ctx) 45 | waitDone := make(chan bool, 1) 46 | defer func() { 47 | cancel() 48 | }() 49 | 50 | go func() { 51 | ui.Say("Waiting for IP...") 52 | ip, err = doGetIp(vm, sub, s.Config) 53 | waitDone <- true 54 | }() 55 | 56 | log.Printf("[INFO] Waiting for IP, up to total timeout: %s, settle timeout: %s", s.Config.WaitTimeout, s.Config.SettleTimeout) 57 | timeout := time.After(s.Config.WaitTimeout) 58 | for { 59 | select { 60 | case <-timeout: 61 | err := fmt.Errorf("Timeout waiting for IP.") 62 | state.Put("error", err) 63 | ui.Error(err.Error()) 64 | cancel() 65 | return multistep.ActionHalt 66 | case <-ctx.Done(): 67 | cancel() 68 | log.Println("[WARN] Interrupt detected, quitting waiting for IP.") 69 | return multistep.ActionHalt 70 | case <-waitDone: 71 | if err != nil { 72 | state.Put("error", err) 73 | return multistep.ActionHalt 74 | } 75 | state.Put("ip", ip) 76 | ui.Say(fmt.Sprintf("IP address: %v", ip)) 77 | return multistep.ActionContinue 78 | case <-time.After(1 * time.Second): 79 | if _, ok := state.GetOk(multistep.StateCancelled); ok { 80 | return multistep.ActionHalt 81 | } 82 | } 83 | } 84 | } 85 | 86 | func doGetIp(vm *driver.VirtualMachine, ctx context.Context, c *WaitIpConfig) (string, error) { 87 | var prevIp = "" 88 | var stopTime time.Time 89 | var interval time.Duration 90 | if c.SettleTimeout.Seconds() >= 120 { 91 | interval = 30 * time.Second 92 | } else if c.SettleTimeout.Seconds() >= 60 { 93 | interval = 15 * time.Second 94 | } else if c.SettleTimeout.Seconds() >= 10 { 95 | interval = 5 * time.Second 96 | } else { 97 | interval = 1 * time.Second 98 | } 99 | loop: 100 | ip, err := vm.WaitForIP(ctx) 101 | if err != nil { 102 | return "", err 103 | } 104 | if prevIp == "" || prevIp != ip { 105 | if prevIp == "" { 106 | log.Printf("VM IP aquired: %s", ip) 107 | } else { 108 | log.Printf("VM IP changed from %s to %s", prevIp, ip) 109 | } 110 | prevIp = ip 111 | stopTime = time.Now().Add(c.SettleTimeout) 112 | goto loop 113 | } else { 114 | log.Printf("VM IP is still the same: %s", prevIp) 115 | if time.Now().After(stopTime) { 116 | log.Printf("VM IP seems stable enough: %s", ip) 117 | return ip, nil 118 | } 119 | select { 120 | case <-ctx.Done(): 121 | return "", fmt.Errorf("IP wait cancelled") 122 | case <-time.After(interval): 123 | goto loop 124 | } 125 | } 126 | 127 | } 128 | 129 | func (s *StepWaitForIp) Cleanup(state multistep.StateBag) {} 130 | -------------------------------------------------------------------------------- /common/testing/utility.go: -------------------------------------------------------------------------------- 1 | package testing 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/hashicorp/packer/packer" 7 | "github.com/jetbrains-infra/packer-builder-vsphere/common" 8 | "github.com/jetbrains-infra/packer-builder-vsphere/driver" 9 | "math/rand" 10 | "os" 11 | "testing" 12 | "time" 13 | ) 14 | 15 | func NewVMName() string { 16 | rand.Seed(time.Now().UnixNano()) 17 | return fmt.Sprintf("test-%v", rand.Intn(1000)) 18 | } 19 | 20 | func RenderConfig(config map[string]interface{}) string { 21 | t := map[string][]map[string]interface{}{ 22 | "builders": { 23 | map[string]interface{}{ 24 | "type": "test", 25 | }, 26 | }, 27 | } 28 | for k, v := range config { 29 | t["builders"][0][k] = v 30 | } 31 | 32 | j, _ := json.Marshal(t) 33 | return string(j) 34 | } 35 | 36 | func TestConn(t *testing.T) *driver.Driver { 37 | username := os.Getenv("VSPHERE_USERNAME") 38 | if username == "" { 39 | username = "root" 40 | } 41 | password := os.Getenv("VSPHERE_PASSWORD") 42 | if password == "" { 43 | password = "jetbrains" 44 | } 45 | 46 | d, err := driver.NewDriver(&driver.ConnectConfig{ 47 | VCenterServer: "vcenter.vsphere65.test", 48 | Username: username, 49 | Password: password, 50 | InsecureConnection: true, 51 | }) 52 | if err != nil { 53 | t.Fatal("Cannot connect: ", err) 54 | } 55 | return d 56 | } 57 | 58 | func GetVM(t *testing.T, d *driver.Driver, artifacts []packer.Artifact) *driver.VirtualMachine { 59 | artifactRaw := artifacts[0] 60 | artifact, _ := artifactRaw.(*common.Artifact) 61 | 62 | vm, err := d.FindVM(artifact.Name) 63 | if err != nil { 64 | t.Fatalf("Cannot find VM: %v", err) 65 | } 66 | 67 | return vm 68 | } 69 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | build: 4 | image: jetbrainsinfra/golang:1.11.4 5 | volumes: 6 | - .:/work 7 | - modules:/go/pkg/mod 8 | - cache:/root/.cache 9 | working_dir: /work 10 | command: make build -j 3 11 | 12 | test: 13 | image: jetbrainsinfra/golang:1.11.4 14 | volumes: 15 | - .:/work 16 | - modules:/go/pkg/mod 17 | - cache:/root/.cache 18 | working_dir: /work 19 | # network_mode: "container:vpn" 20 | environment: 21 | VSPHERE_USERNAME: 22 | VSPHERE_PASSWORD: 23 | command: make test 24 | 25 | volumes: 26 | modules: 27 | cache: 28 | -------------------------------------------------------------------------------- /driver/datastore.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import ( 4 | "fmt" 5 | "github.com/vmware/govmomi/object" 6 | "github.com/vmware/govmomi/vim25/mo" 7 | "github.com/vmware/govmomi/vim25/soap" 8 | "github.com/vmware/govmomi/vim25/types" 9 | ) 10 | 11 | type Datastore struct { 12 | ds *object.Datastore 13 | driver *Driver 14 | } 15 | 16 | func (d *Driver) NewDatastore(ref *types.ManagedObjectReference) *Datastore { 17 | return &Datastore{ 18 | ds: object.NewDatastore(d.client.Client, *ref), 19 | driver: d, 20 | } 21 | } 22 | 23 | // If name is an empty string, then resolve host's one 24 | func (d *Driver) FindDatastore(name string, host string) (*Datastore, error) { 25 | if name == "" { 26 | h, err := d.FindHost(host) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | i, err := h.Info("datastore") 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | if len(i.Datastore) > 1 { 37 | return nil, fmt.Errorf("Host has multiple datastores. Specify it explicitly") 38 | } 39 | 40 | ds := d.NewDatastore(&i.Datastore[0]) 41 | inf, err := ds.Info("name") 42 | if err != nil { 43 | return nil, err 44 | } 45 | name = inf.Name 46 | } 47 | 48 | ds, err := d.finder.Datastore(d.ctx, name) 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | return &Datastore{ 54 | ds: ds, 55 | driver: d, 56 | }, nil 57 | } 58 | 59 | func (ds *Datastore) Info(params ...string) (*mo.Datastore, error) { 60 | var p []string 61 | if len(params) == 0 { 62 | p = []string{"*"} 63 | } else { 64 | p = params 65 | } 66 | var info mo.Datastore 67 | err := ds.ds.Properties(ds.driver.ctx, ds.ds.Reference(), p, &info) 68 | if err != nil { 69 | return nil, err 70 | } 71 | return &info, nil 72 | } 73 | 74 | func (ds *Datastore) FileExists(path string) bool { 75 | _, err := ds.ds.Stat(ds.driver.ctx, path) 76 | return err == nil 77 | } 78 | 79 | func (ds *Datastore) Name() string { 80 | return ds.ds.Name() 81 | } 82 | 83 | func (ds *Datastore) ResolvePath(path string) string { 84 | return ds.ds.Path(path) 85 | } 86 | 87 | func (ds *Datastore) UploadFile(src, dst string, host string) error { 88 | p := soap.DefaultUpload 89 | ctx := ds.driver.ctx 90 | 91 | if host != "" { 92 | h, err := ds.driver.FindHost(host) 93 | if err != nil { 94 | return err 95 | } 96 | ctx = ds.ds.HostContext(ctx, h.host) 97 | } 98 | 99 | return ds.ds.UploadFile(ctx, src, dst, &p) 100 | } 101 | 102 | func (ds *Datastore) Delete(path string) error { 103 | dc, err := ds.driver.finder.Datacenter(ds.driver.ctx, ds.ds.DatacenterPath) 104 | if err != nil { 105 | return err 106 | } 107 | fm := ds.ds.NewFileManager(dc, false) 108 | return fm.Delete(ds.driver.ctx, path) 109 | } 110 | 111 | func (ds *Datastore) MakeDirectory(path string) error { 112 | dc, err := ds.driver.finder.Datacenter(ds.driver.ctx, ds.ds.DatacenterPath) 113 | if err != nil { 114 | return err 115 | } 116 | fm := ds.ds.NewFileManager(dc, false) 117 | return fm.FileManager.MakeDirectory(ds.driver.ctx, path, dc, true) 118 | } 119 | 120 | // Cuts out the datastore prefix 121 | // Example: "[datastore1] file.ext" --> "file.ext" 122 | func RemoveDatastorePrefix(path string) string { 123 | res := object.DatastorePath{} 124 | if hadPrefix := res.FromString(path); hadPrefix { 125 | return res.Path 126 | } else { 127 | return path 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /driver/datastore_acc_test.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestDatastoreAcc(t *testing.T) { 11 | d := newTestDriver(t) 12 | ds, err := d.FindDatastore("datastore1", "") 13 | if err != nil { 14 | t.Fatalf("Cannot find the default datastore '%v': %v", "datastore1", err) 15 | } 16 | info, err := ds.Info("name") 17 | if err != nil { 18 | t.Fatalf("Cannot read datastore properties: %v", err) 19 | } 20 | if info.Name != "datastore1" { 21 | t.Errorf("Wrong datastore. expected: 'datastore1', got: '%v'", info.Name) 22 | } 23 | } 24 | 25 | func TestFileUpload(t *testing.T) { 26 | dsName := "datastore1" 27 | hostName := "esxi-1.vsphere65.test" 28 | 29 | fileName := fmt.Sprintf("test-%v", time.Now().Unix()) 30 | tmpFile, err := ioutil.TempFile("", fileName) 31 | if err != nil { 32 | t.Fatalf("Error creating temp file") 33 | } 34 | err = tmpFile.Close() 35 | if err != nil { 36 | t.Fatalf("Error creating temp file") 37 | } 38 | 39 | d := newTestDriver(t) 40 | ds, err := d.FindDatastore(dsName, hostName) 41 | if err != nil { 42 | t.Fatalf("Cannot find datastore '%v': %v", dsName, err) 43 | } 44 | 45 | err = ds.UploadFile(tmpFile.Name(), fileName, hostName) 46 | if err != nil { 47 | t.Fatalf("Cannot upload file: %v", err) 48 | } 49 | 50 | if ds.FileExists(fileName) != true { 51 | t.Fatalf("Cannot find file") 52 | } 53 | 54 | err = ds.Delete(fileName) 55 | if err != nil { 56 | t.Fatalf("Cannot delete file: %v", err) 57 | } 58 | } 59 | 60 | func TestFileUploadDRS(t *testing.T) { 61 | dsName := "datastore3" 62 | hostName := "" 63 | 64 | fileName := fmt.Sprintf("test-%v", time.Now().Unix()) 65 | tmpFile, err := ioutil.TempFile("", fileName) 66 | if err != nil { 67 | t.Fatalf("Error creating temp file") 68 | } 69 | err = tmpFile.Close() 70 | if err != nil { 71 | t.Fatalf("Error creating temp file") 72 | } 73 | 74 | d := newTestDriver(t) 75 | ds, err := d.FindDatastore(dsName, hostName) 76 | if err != nil { 77 | t.Fatalf("Cannot find datastore '%v': %v", dsName, err) 78 | } 79 | 80 | err = ds.UploadFile(tmpFile.Name(), fileName, hostName) 81 | if err != nil { 82 | t.Fatalf("Cannot upload file: %v", err) 83 | } 84 | 85 | if ds.FileExists(fileName) != true { 86 | t.Fatalf("Cannot find file") 87 | } 88 | 89 | err = ds.Delete(fileName) 90 | if err != nil { 91 | t.Fatalf("Cannot delete file: %v", err) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /driver/driver.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/vmware/govmomi" 7 | "github.com/vmware/govmomi/find" 8 | "github.com/vmware/govmomi/object" 9 | "github.com/vmware/govmomi/session" 10 | "github.com/vmware/govmomi/vim25" 11 | "github.com/vmware/govmomi/vim25/soap" 12 | "net/url" 13 | "time" 14 | ) 15 | 16 | type Driver struct { 17 | ctx context.Context 18 | client *govmomi.Client 19 | finder *find.Finder 20 | datacenter *object.Datacenter 21 | } 22 | 23 | type ConnectConfig struct { 24 | VCenterServer string 25 | Username string 26 | Password string 27 | InsecureConnection bool 28 | Datacenter string 29 | } 30 | 31 | func NewDriver(config *ConnectConfig) (*Driver, error) { 32 | ctx := context.TODO() 33 | 34 | vcenterUrl, err := url.Parse(fmt.Sprintf("https://%v/sdk", config.VCenterServer)) 35 | if err != nil { 36 | return nil, err 37 | } 38 | credentials := url.UserPassword(config.Username, config.Password) 39 | vcenterUrl.User = credentials 40 | 41 | soapClient := soap.NewClient(vcenterUrl, config.InsecureConnection) 42 | vimClient, err := vim25.NewClient(ctx, soapClient) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | vimClient.RoundTripper = session.KeepAlive(vimClient.RoundTripper, 10*time.Minute) 48 | client := &govmomi.Client{ 49 | Client: vimClient, 50 | SessionManager: session.NewManager(vimClient), 51 | } 52 | 53 | err = client.SessionManager.Login(ctx, credentials) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | finder := find.NewFinder(client.Client, false) 59 | datacenter, err := finder.DatacenterOrDefault(ctx, config.Datacenter) 60 | if err != nil { 61 | return nil, err 62 | } 63 | finder.SetDatacenter(datacenter) 64 | 65 | d := Driver{ 66 | ctx: ctx, 67 | client: client, 68 | datacenter: datacenter, 69 | finder: finder, 70 | } 71 | return &d, nil 72 | } 73 | -------------------------------------------------------------------------------- /driver/driver_test.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "os" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | // Defines whether acceptance tests should be run 12 | const TestHostName = "esxi-1.vsphere65.test" 13 | 14 | func newTestDriver(t *testing.T) *Driver { 15 | username := os.Getenv("VSPHERE_USERNAME") 16 | if username == "" { 17 | username = "root" 18 | } 19 | password := os.Getenv("VSPHERE_PASSWORD") 20 | if password == "" { 21 | password = "jetbrains" 22 | } 23 | 24 | d, err := NewDriver(&ConnectConfig{ 25 | VCenterServer: "vcenter.vsphere65.test", 26 | Username: username, 27 | Password: password, 28 | InsecureConnection: true, 29 | }) 30 | if err != nil { 31 | t.Fatalf("Cannot connect: %v", err) 32 | } 33 | return d 34 | } 35 | 36 | func newVMName() string { 37 | rand.Seed(time.Now().UTC().UnixNano()) 38 | return fmt.Sprintf("test-%v", rand.Intn(1000)) 39 | } 40 | -------------------------------------------------------------------------------- /driver/folder.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import ( 4 | "fmt" 5 | "github.com/vmware/govmomi/object" 6 | "github.com/vmware/govmomi/vim25/mo" 7 | "github.com/vmware/govmomi/vim25/types" 8 | ) 9 | 10 | type Folder struct { 11 | driver *Driver 12 | folder *object.Folder 13 | } 14 | 15 | func (d *Driver) NewFolder(ref *types.ManagedObjectReference) *Folder { 16 | return &Folder{ 17 | folder: object.NewFolder(d.client.Client, *ref), 18 | driver: d, 19 | } 20 | } 21 | 22 | func (d *Driver) FindFolder(name string) (*Folder, error) { 23 | f, err := d.finder.Folder(d.ctx, fmt.Sprintf("/%v/vm/%v", d.datacenter.Name(), name)) 24 | if err != nil { 25 | return nil, err 26 | } 27 | return &Folder{ 28 | folder: f, 29 | driver: d, 30 | }, nil 31 | } 32 | 33 | func (f *Folder) Info(params ...string) (*mo.Folder, error) { 34 | var p []string 35 | if len(params) == 0 { 36 | p = []string{"*"} 37 | } else { 38 | p = params 39 | } 40 | var info mo.Folder 41 | err := f.folder.Properties(f.driver.ctx, f.folder.Reference(), p, &info) 42 | if err != nil { 43 | return nil, err 44 | } 45 | return &info, nil 46 | } 47 | 48 | func (f *Folder) Path() (string, error) { 49 | info, err := f.Info("name", "parent") 50 | if err != nil { 51 | return "", err 52 | } 53 | if info.Parent.Type == "Datacenter" { 54 | return "", nil 55 | } else { 56 | parent := f.driver.NewFolder(info.Parent) 57 | path, err := parent.Path() 58 | if err != nil { 59 | return "", err 60 | } 61 | if path == "" { 62 | return info.Name, nil 63 | } else { 64 | return fmt.Sprintf("%v/%v", path, info.Name), nil 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /driver/folder_acc_test.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import "testing" 4 | 5 | func TestFolderAcc(t *testing.T) { 6 | d := newTestDriver(t) 7 | f, err := d.FindFolder("folder1/folder2") 8 | if err != nil { 9 | t.Fatalf("Cannot find the default folder '%v': %v", "folder1/folder2", err) 10 | } 11 | path, err := f.Path() 12 | if err != nil { 13 | t.Fatalf("Cannot read folder name: %v", err) 14 | } 15 | if path != "folder1/folder2" { 16 | t.Errorf("Wrong folder. expected: 'folder1/folder2', got: '%v'", path) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /driver/host.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import ( 4 | "github.com/vmware/govmomi/object" 5 | "github.com/vmware/govmomi/vim25/mo" 6 | "github.com/vmware/govmomi/vim25/types" 7 | ) 8 | 9 | type Host struct { 10 | driver *Driver 11 | host *object.HostSystem 12 | } 13 | 14 | func (d *Driver) NewHost(ref *types.ManagedObjectReference) *Host { 15 | return &Host{ 16 | host: object.NewHostSystem(d.client.Client, *ref), 17 | driver: d, 18 | } 19 | } 20 | 21 | func (d *Driver) FindHost(name string) (*Host, error) { 22 | h, err := d.finder.HostSystem(d.ctx, name) 23 | if err != nil { 24 | return nil, err 25 | } 26 | return &Host{ 27 | host: h, 28 | driver: d, 29 | }, nil 30 | } 31 | 32 | func (h *Host) Info(params ...string) (*mo.HostSystem, error) { 33 | var p []string 34 | if len(params) == 0 { 35 | p = []string{"*"} 36 | } else { 37 | p = params 38 | } 39 | var info mo.HostSystem 40 | err := h.host.Properties(h.driver.ctx, h.host.Reference(), p, &info) 41 | if err != nil { 42 | return nil, err 43 | } 44 | return &info, nil 45 | } 46 | -------------------------------------------------------------------------------- /driver/host_acc_test.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestHostAcc(t *testing.T) { 8 | d := newTestDriver(t) 9 | host, err := d.FindHost(TestHostName) 10 | if err != nil { 11 | t.Fatalf("Cannot find the default host '%v': %v", "datastore1", err) 12 | } 13 | 14 | info, err := host.Info("name") 15 | if err != nil { 16 | t.Fatalf("Cannot read host properties: %v", err) 17 | } 18 | if info.Name != TestHostName { 19 | t.Errorf("Wrong host name: expected '%v', got: '%v'", TestHostName, info.Name) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /driver/leak_test.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import "testing" 4 | import "go.uber.org/goleak" 5 | 6 | func TestMain(m *testing.M) { 7 | goleak.VerifyTestMain(m, goleak.IgnoreTopFunction("go.opencensus.io/stats/view.(*worker).start")) 8 | } 9 | -------------------------------------------------------------------------------- /driver/network.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import ( 4 | "github.com/vmware/govmomi/object" 5 | "github.com/vmware/govmomi/vim25/mo" 6 | "github.com/vmware/govmomi/vim25/types" 7 | ) 8 | 9 | type Network struct { 10 | driver *Driver 11 | network *object.Network 12 | } 13 | 14 | func (d *Driver) NewNetwork(ref *types.ManagedObjectReference) *Network { 15 | return &Network{ 16 | network: object.NewNetwork(d.client.Client, *ref), 17 | driver: d, 18 | } 19 | } 20 | 21 | func (d *Driver) FindNetwork(name string) (*Network, error) { 22 | n, err := d.finder.Network(d.ctx, name) 23 | if err != nil { 24 | return nil, err 25 | } 26 | return &Network{ 27 | network: n.(*object.Network), 28 | driver: d, 29 | }, nil 30 | } 31 | 32 | func (n *Network) Info(params ...string) (*mo.Network, error) { 33 | var p []string 34 | if len(params) == 0 { 35 | p = []string{"*"} 36 | } else { 37 | p = params 38 | } 39 | var info mo.Network 40 | err := n.network.Properties(n.driver.ctx, n.network.Reference(), p, &info) 41 | if err != nil { 42 | return nil, err 43 | } 44 | return &info, nil 45 | } 46 | -------------------------------------------------------------------------------- /driver/resource_pool.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import ( 4 | "fmt" 5 | "github.com/vmware/govmomi/object" 6 | "github.com/vmware/govmomi/vim25/mo" 7 | "github.com/vmware/govmomi/vim25/types" 8 | ) 9 | 10 | type ResourcePool struct { 11 | pool *object.ResourcePool 12 | driver *Driver 13 | } 14 | 15 | func (d *Driver) NewResourcePool(ref *types.ManagedObjectReference) *ResourcePool { 16 | return &ResourcePool{ 17 | pool: object.NewResourcePool(d.client.Client, *ref), 18 | driver: d, 19 | } 20 | } 21 | 22 | func (d *Driver) FindResourcePool(cluster string, host string, name string) (*ResourcePool, error) { 23 | var res string 24 | if cluster != "" { 25 | res = cluster 26 | } else { 27 | res = host 28 | } 29 | 30 | p, err := d.finder.ResourcePool(d.ctx, fmt.Sprintf("%v/Resources/%v", res, name)) 31 | if err != nil { 32 | return nil, err 33 | } 34 | return &ResourcePool{ 35 | pool: p, 36 | driver: d, 37 | }, nil 38 | } 39 | 40 | func (p *ResourcePool) Info(params ...string) (*mo.ResourcePool, error) { 41 | var params2 []string 42 | if len(params) == 0 { 43 | params2 = []string{"*"} 44 | } else { 45 | params2 = params 46 | } 47 | var info mo.ResourcePool 48 | err := p.pool.Properties(p.driver.ctx, p.pool.Reference(), params2, &info) 49 | if err != nil { 50 | return nil, err 51 | } 52 | return &info, nil 53 | } 54 | 55 | func (p *ResourcePool) Path() (string, error) { 56 | poolInfo, err := p.Info("name", "parent") 57 | if err != nil { 58 | return "", err 59 | } 60 | if poolInfo.Parent.Type == "ComputeResource" { 61 | return "", nil 62 | } else { 63 | parent := p.driver.NewResourcePool(poolInfo.Parent) 64 | parentPath, err := parent.Path() 65 | if err != nil { 66 | return "", err 67 | } 68 | if parentPath == "" { 69 | return poolInfo.Name, nil 70 | } else { 71 | return fmt.Sprintf("%v/%v", parentPath, poolInfo.Name), nil 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /driver/resource_pool_acc_test.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import "testing" 4 | 5 | func TestResourcePoolAcc(t *testing.T) { 6 | d := newTestDriver(t) 7 | p, err := d.FindResourcePool("", "esxi-1.vsphere65.test", "pool1/pool2") 8 | if err != nil { 9 | t.Fatalf("Cannot find the default resource pool '%v': %v", "pool1/pool2", err) 10 | } 11 | 12 | path, err := p.Path() 13 | if err != nil { 14 | t.Fatalf("Cannot read resource pool name: %v", err) 15 | } 16 | if path != "pool1/pool2" { 17 | t.Errorf("Wrong folder. expected: 'pool1/pool2', got: '%v'", path) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /driver/vm.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "github.com/vmware/govmomi/object" 8 | "github.com/vmware/govmomi/vim25/mo" 9 | "github.com/vmware/govmomi/vim25/types" 10 | "log" 11 | "strings" 12 | "time" 13 | ) 14 | 15 | type VirtualMachine struct { 16 | vm *object.VirtualMachine 17 | driver *Driver 18 | } 19 | 20 | type CloneConfig struct { 21 | Name string 22 | Folder string 23 | Cluster string 24 | Host string 25 | ResourcePool string 26 | Datastore string 27 | LinkedClone bool 28 | Network string 29 | Annotation string 30 | } 31 | 32 | type HardwareConfig struct { 33 | CPUs int32 34 | CpuCores int32 35 | CPUReservation int64 36 | CPULimit int64 37 | RAM int64 38 | RAMReservation int64 39 | RAMReserveAll bool 40 | NestedHV bool 41 | CpuHotAddEnabled bool 42 | MemoryHotAddEnabled bool 43 | VideoRAM int64 44 | } 45 | 46 | type CreateConfig struct { 47 | DiskThinProvisioned bool 48 | DiskControllerType string // example: "scsi", "pvscsi" 49 | DiskSize int64 50 | 51 | Annotation string 52 | Name string 53 | Folder string 54 | Cluster string 55 | Host string 56 | ResourcePool string 57 | Datastore string 58 | GuestOS string // example: otherGuest 59 | Network string // "" for default network 60 | NetworkCard string // example: vmxnet3 61 | USBController bool 62 | Version uint // example: 10 63 | Firmware string // efi or bios 64 | } 65 | 66 | func (d *Driver) NewVM(ref *types.ManagedObjectReference) *VirtualMachine { 67 | return &VirtualMachine{ 68 | vm: object.NewVirtualMachine(d.client.Client, *ref), 69 | driver: d, 70 | } 71 | } 72 | 73 | func (d *Driver) FindVM(name string) (*VirtualMachine, error) { 74 | vm, err := d.finder.VirtualMachine(d.ctx, name) 75 | if err != nil { 76 | return nil, err 77 | } 78 | return &VirtualMachine{ 79 | vm: vm, 80 | driver: d, 81 | }, nil 82 | } 83 | 84 | func (d *Driver) CreateVM(config *CreateConfig) (*VirtualMachine, error) { 85 | createSpec := types.VirtualMachineConfigSpec{ 86 | Name: config.Name, 87 | Annotation: config.Annotation, 88 | GuestId: config.GuestOS, 89 | } 90 | if config.Version != 0 { 91 | createSpec.Version = fmt.Sprintf("%s%d", "vmx-", config.Version) 92 | } 93 | if config.Firmware != "" { 94 | createSpec.Firmware = config.Firmware 95 | } 96 | 97 | folder, err := d.FindFolder(config.Folder) 98 | if err != nil { 99 | return nil, err 100 | } 101 | 102 | resourcePool, err := d.FindResourcePool(config.Cluster, config.Host, config.ResourcePool) 103 | if err != nil { 104 | return nil, err 105 | } 106 | 107 | var host *object.HostSystem 108 | if config.Cluster != "" && config.Host != "" { 109 | h, err := d.FindHost(config.Host) 110 | if err != nil { 111 | return nil, err 112 | } 113 | host = h.host 114 | } 115 | 116 | datastore, err := d.FindDatastore(config.Datastore, config.Host) 117 | if err != nil { 118 | return nil, err 119 | } 120 | 121 | devices := object.VirtualDeviceList{} 122 | 123 | devices, err = addDisk(d, devices, config) 124 | if err != nil { 125 | return nil, err 126 | } 127 | devices, err = addNetwork(d, devices, config) 128 | if err != nil { 129 | return nil, err 130 | } 131 | 132 | if config.USBController { 133 | t := true 134 | usb := &types.VirtualUSBController{ 135 | EhciEnabled: &t, 136 | } 137 | devices = append(devices, usb) 138 | } 139 | 140 | createSpec.DeviceChange, err = devices.ConfigSpec(types.VirtualDeviceConfigSpecOperationAdd) 141 | if err != nil { 142 | return nil, err 143 | } 144 | 145 | createSpec.Files = &types.VirtualMachineFileInfo{ 146 | VmPathName: fmt.Sprintf("[%s]", datastore.Name()), 147 | } 148 | 149 | task, err := folder.folder.CreateVM(d.ctx, createSpec, resourcePool.pool, host) 150 | if err != nil { 151 | return nil, err 152 | } 153 | taskInfo, err := task.WaitForResult(d.ctx, nil) 154 | if err != nil { 155 | return nil, err 156 | } 157 | 158 | vmRef := taskInfo.Result.(types.ManagedObjectReference) 159 | 160 | return d.NewVM(&vmRef), nil 161 | } 162 | 163 | func (vm *VirtualMachine) Info(params ...string) (*mo.VirtualMachine, error) { 164 | var p []string 165 | if len(params) == 0 { 166 | p = []string{"*"} 167 | } else { 168 | p = params 169 | } 170 | var info mo.VirtualMachine 171 | err := vm.vm.Properties(vm.driver.ctx, vm.vm.Reference(), p, &info) 172 | if err != nil { 173 | return nil, err 174 | } 175 | return &info, nil 176 | } 177 | 178 | func (vm *VirtualMachine) Devices() (object.VirtualDeviceList, error) { 179 | vmInfo, err := vm.Info("config.hardware.device") 180 | if err != nil { 181 | return nil, err 182 | } 183 | 184 | return vmInfo.Config.Hardware.Device, nil 185 | } 186 | 187 | func (vm *VirtualMachine) Clone(ctx context.Context, config *CloneConfig) (*VirtualMachine, error) { 188 | folder, err := vm.driver.FindFolder(config.Folder) 189 | if err != nil { 190 | return nil, err 191 | } 192 | 193 | var relocateSpec types.VirtualMachineRelocateSpec 194 | 195 | pool, err := vm.driver.FindResourcePool(config.Cluster, config.Host, config.ResourcePool) 196 | if err != nil { 197 | return nil, err 198 | } 199 | poolRef := pool.pool.Reference() 200 | relocateSpec.Pool = &poolRef 201 | 202 | datastore, err := vm.driver.FindDatastore(config.Datastore, config.Host) 203 | if err != nil { 204 | return nil, err 205 | } 206 | datastoreRef := datastore.ds.Reference() 207 | relocateSpec.Datastore = &datastoreRef 208 | 209 | var cloneSpec types.VirtualMachineCloneSpec 210 | cloneSpec.Location = relocateSpec 211 | cloneSpec.PowerOn = false 212 | 213 | if config.LinkedClone == true { 214 | cloneSpec.Location.DiskMoveType = "createNewChildDiskBacking" 215 | 216 | tpl, err := vm.Info("snapshot") 217 | if err != nil { 218 | return nil, err 219 | } 220 | if tpl.Snapshot == nil { 221 | err = errors.New("`linked_clone=true`, but template has no snapshots") 222 | return nil, err 223 | } 224 | cloneSpec.Snapshot = tpl.Snapshot.CurrentSnapshot 225 | } 226 | 227 | var configSpec types.VirtualMachineConfigSpec 228 | cloneSpec.Config = &configSpec 229 | 230 | if config.Annotation != "" { 231 | configSpec.Annotation = config.Annotation 232 | } 233 | 234 | if config.Network != "" { 235 | net, err := vm.driver.FindNetwork(config.Network) 236 | if err != nil { 237 | return nil, err 238 | } 239 | backing, err := net.network.EthernetCardBackingInfo(ctx) 240 | if err != nil { 241 | return nil, err 242 | } 243 | 244 | devices, err := vm.vm.Device(ctx) 245 | if err != nil { 246 | return nil, err 247 | } 248 | 249 | adapter, err := findNetworkAdapter(devices) 250 | if err != nil { 251 | return nil, err 252 | } 253 | 254 | adapter.GetVirtualEthernetCard().Backing = backing 255 | 256 | config := &types.VirtualDeviceConfigSpec{ 257 | Device: adapter.(types.BaseVirtualDevice), 258 | Operation: types.VirtualDeviceConfigSpecOperationEdit, 259 | } 260 | 261 | configSpec.DeviceChange = append(configSpec.DeviceChange, config) 262 | } 263 | 264 | task, err := vm.vm.Clone(vm.driver.ctx, folder.folder, config.Name, cloneSpec) 265 | if err != nil { 266 | return nil, err 267 | } 268 | 269 | info, err := task.WaitForResult(ctx, nil) 270 | if err != nil { 271 | if ctx.Err() == context.Canceled { 272 | err = task.Cancel(context.TODO()) 273 | return nil, err 274 | } 275 | 276 | return nil, err 277 | } 278 | 279 | vmRef := info.Result.(types.ManagedObjectReference) 280 | created := vm.driver.NewVM(&vmRef) 281 | return created, nil 282 | } 283 | 284 | func (vm *VirtualMachine) Destroy() error { 285 | task, err := vm.vm.Destroy(vm.driver.ctx) 286 | if err != nil { 287 | return err 288 | } 289 | _, err = task.WaitForResult(vm.driver.ctx, nil) 290 | return err 291 | } 292 | 293 | func (vm *VirtualMachine) Configure(config *HardwareConfig) error { 294 | var confSpec types.VirtualMachineConfigSpec 295 | confSpec.NumCPUs = config.CPUs 296 | confSpec.NumCoresPerSocket = config.CpuCores 297 | confSpec.MemoryMB = config.RAM 298 | 299 | var cpuSpec types.ResourceAllocationInfo 300 | cpuSpec.Reservation = &config.CPUReservation 301 | if config.CPULimit != 0 { 302 | cpuSpec.Limit = &config.CPULimit 303 | } 304 | confSpec.CpuAllocation = &cpuSpec 305 | 306 | var ramSpec types.ResourceAllocationInfo 307 | ramSpec.Reservation = &config.RAMReservation 308 | confSpec.MemoryAllocation = &ramSpec 309 | 310 | confSpec.MemoryReservationLockedToMax = &config.RAMReserveAll 311 | confSpec.NestedHVEnabled = &config.NestedHV 312 | 313 | confSpec.CpuHotAddEnabled = &config.CpuHotAddEnabled 314 | confSpec.MemoryHotAddEnabled = &config.MemoryHotAddEnabled 315 | 316 | if config.VideoRAM != 0 { 317 | devices, err := vm.vm.Device(vm.driver.ctx) 318 | if err != nil { 319 | return err 320 | } 321 | l := devices.SelectByType((*types.VirtualMachineVideoCard)(nil)) 322 | if len(l) != 1 { 323 | return err 324 | } 325 | card := l[0].(*types.VirtualMachineVideoCard) 326 | 327 | card.VideoRamSizeInKB = config.VideoRAM 328 | 329 | spec := &types.VirtualDeviceConfigSpec{ 330 | Device: card, 331 | Operation: types.VirtualDeviceConfigSpecOperationEdit, 332 | } 333 | confSpec.DeviceChange = append(confSpec.DeviceChange, spec) 334 | } 335 | 336 | task, err := vm.vm.Reconfigure(vm.driver.ctx, confSpec) 337 | if err != nil { 338 | return err 339 | } 340 | 341 | _, err = task.WaitForResult(vm.driver.ctx, nil) 342 | return err 343 | } 344 | 345 | func (vm *VirtualMachine) ResizeDisk(diskSize int64) error { 346 | var confSpec types.VirtualMachineConfigSpec 347 | 348 | devices, err := vm.vm.Device(vm.driver.ctx) 349 | if err != nil { 350 | return err 351 | } 352 | 353 | disk, err := findDisk(devices) 354 | if err != nil { 355 | return err 356 | } 357 | 358 | disk.CapacityInKB = diskSize * 1024 359 | 360 | confSpec.DeviceChange = []types.BaseVirtualDeviceConfigSpec{ 361 | &types.VirtualDeviceConfigSpec{ 362 | Device: disk, 363 | Operation: types.VirtualDeviceConfigSpecOperationEdit, 364 | }, 365 | } 366 | 367 | task, err := vm.vm.Reconfigure(vm.driver.ctx, confSpec) 368 | if err != nil { 369 | return err 370 | } 371 | 372 | _, err = task.WaitForResult(vm.driver.ctx, nil) 373 | return err 374 | } 375 | 376 | func findDisk(devices object.VirtualDeviceList) (*types.VirtualDisk, error) { 377 | var disks []*types.VirtualDisk 378 | for _, device := range devices { 379 | switch d := device.(type) { 380 | case *types.VirtualDisk: 381 | disks = append(disks, d) 382 | } 383 | } 384 | 385 | switch len(disks) { 386 | case 0: 387 | return nil, errors.New("VM has no disks") 388 | case 1: 389 | return disks[0], nil 390 | } 391 | return nil, errors.New("VM has multiple disks") 392 | } 393 | 394 | func (vm *VirtualMachine) PowerOn() error { 395 | task, err := vm.vm.PowerOn(vm.driver.ctx) 396 | if err != nil { 397 | return err 398 | } 399 | _, err = task.WaitForResult(vm.driver.ctx, nil) 400 | return err 401 | } 402 | 403 | func (vm *VirtualMachine) WaitForIP(ctx context.Context) (string, error) { 404 | return vm.vm.WaitForIP(ctx) 405 | } 406 | 407 | func (vm *VirtualMachine) PowerOff() error { 408 | state, err := vm.vm.PowerState(vm.driver.ctx) 409 | if err != nil { 410 | return err 411 | } 412 | 413 | if state == types.VirtualMachinePowerStatePoweredOff { 414 | return nil 415 | } 416 | 417 | task, err := vm.vm.PowerOff(vm.driver.ctx) 418 | if err != nil { 419 | return err 420 | } 421 | _, err = task.WaitForResult(vm.driver.ctx, nil) 422 | return err 423 | } 424 | 425 | func (vm *VirtualMachine) StartShutdown() error { 426 | err := vm.vm.ShutdownGuest(vm.driver.ctx) 427 | return err 428 | } 429 | 430 | func (vm *VirtualMachine) WaitForShutdown(ctx context.Context, timeout time.Duration) error { 431 | shutdownTimer := time.After(timeout) 432 | for { 433 | powerState, err := vm.vm.PowerState(vm.driver.ctx) 434 | if err != nil { 435 | return err 436 | } 437 | if powerState == "poweredOff" { 438 | break 439 | } 440 | 441 | select { 442 | case <-shutdownTimer: 443 | err := errors.New("Timeout while waiting for machine to shut down.") 444 | return err 445 | case <-ctx.Done(): 446 | return nil 447 | default: 448 | time.Sleep(1 * time.Second) 449 | } 450 | } 451 | return nil 452 | } 453 | 454 | func (vm *VirtualMachine) CreateSnapshot(name string) error { 455 | task, err := vm.vm.CreateSnapshot(vm.driver.ctx, name, "", false, false) 456 | if err != nil { 457 | return err 458 | } 459 | _, err = task.WaitForResult(vm.driver.ctx, nil) 460 | return err 461 | } 462 | 463 | func (vm *VirtualMachine) ConvertToTemplate() error { 464 | return vm.vm.MarkAsTemplate(vm.driver.ctx) 465 | } 466 | 467 | func (vm *VirtualMachine) GetDir() (string, error) { 468 | vmInfo, err := vm.Info("name", "layoutEx.file") 469 | if err != nil { 470 | return "", err 471 | } 472 | 473 | vmxName := fmt.Sprintf("/%s.vmx", vmInfo.Name) 474 | for _, file := range vmInfo.LayoutEx.File { 475 | if strings.HasSuffix(file.Name, vmxName) { 476 | return RemoveDatastorePrefix(file.Name[:len(file.Name)-len(vmxName)]), nil 477 | } 478 | } 479 | return "", fmt.Errorf("cannot find '%s'", vmxName) 480 | } 481 | 482 | func addDisk(_ *Driver, devices object.VirtualDeviceList, config *CreateConfig) (object.VirtualDeviceList, error) { 483 | device, err := devices.CreateSCSIController(config.DiskControllerType) 484 | if err != nil { 485 | return nil, err 486 | } 487 | devices = append(devices, device) 488 | controller, err := devices.FindDiskController(devices.Name(device)) 489 | if err != nil { 490 | return nil, err 491 | } 492 | 493 | disk := &types.VirtualDisk{ 494 | VirtualDevice: types.VirtualDevice{ 495 | Key: devices.NewKey(), 496 | Backing: &types.VirtualDiskFlatVer2BackingInfo{ 497 | DiskMode: string(types.VirtualDiskModePersistent), 498 | ThinProvisioned: types.NewBool(config.DiskThinProvisioned), 499 | }, 500 | }, 501 | CapacityInKB: config.DiskSize * 1024, 502 | } 503 | 504 | devices.AssignController(disk, controller) 505 | devices = append(devices, disk) 506 | 507 | return devices, nil 508 | } 509 | 510 | func addNetwork(d *Driver, devices object.VirtualDeviceList, config *CreateConfig) (object.VirtualDeviceList, error) { 511 | var network object.NetworkReference 512 | if config.Network == "" { 513 | h, err := d.FindHost(config.Host) 514 | if err != nil { 515 | return nil, err 516 | } 517 | 518 | i, err := h.Info("network") 519 | if err != nil { 520 | return nil, err 521 | } 522 | 523 | if len(i.Network) > 1 { 524 | return nil, fmt.Errorf("Host has multiple networks. Specify it explicitly") 525 | } 526 | 527 | network = object.NewNetwork(d.client.Client, i.Network[0]) 528 | } else { 529 | var err error 530 | network, err = d.finder.Network(d.ctx, config.Network) 531 | if err != nil { 532 | return nil, err 533 | } 534 | } 535 | 536 | backing, err := network.EthernetCardBackingInfo(d.ctx) 537 | if err != nil { 538 | return nil, err 539 | } 540 | 541 | device, err := object.EthernetCardTypes().CreateEthernetCard(config.NetworkCard, backing) 542 | if err != nil { 543 | return nil, err 544 | } 545 | 546 | return append(devices, device), nil 547 | } 548 | 549 | func (vm *VirtualMachine) AddCdrom(controllerType string, isoPath string) error { 550 | devices, err := vm.vm.Device(vm.driver.ctx) 551 | if err != nil { 552 | return err 553 | } 554 | 555 | var controller *types.VirtualController 556 | if controllerType == "sata" { 557 | c, err := vm.FindSATAController() 558 | if err != nil { 559 | return err 560 | } 561 | controller = c.GetVirtualController() 562 | } else { 563 | c, err := devices.FindIDEController("") 564 | if err != nil { 565 | return err 566 | } 567 | controller = c.GetVirtualController() 568 | } 569 | 570 | cdrom, err := vm.CreateCdrom(controller) 571 | if err != nil { 572 | return err 573 | } 574 | 575 | if isoPath != "" { 576 | devices.InsertIso(cdrom, isoPath) 577 | } 578 | 579 | log.Printf("Creating CD-ROM on controller '%v' with iso '%v'", controller, isoPath) 580 | return vm.addDevice(cdrom) 581 | } 582 | 583 | func (vm *VirtualMachine) AddFloppy(imgPath string) error { 584 | devices, err := vm.vm.Device(vm.driver.ctx) 585 | if err != nil { 586 | return err 587 | } 588 | 589 | floppy, err := devices.CreateFloppy() 590 | if err != nil { 591 | return err 592 | } 593 | 594 | if imgPath != "" { 595 | floppy = devices.InsertImg(floppy, imgPath) 596 | } 597 | 598 | return vm.addDevice(floppy) 599 | } 600 | 601 | func (vm *VirtualMachine) SetBootOrder(order []string) error { 602 | devices, err := vm.vm.Device(vm.driver.ctx) 603 | if err != nil { 604 | return err 605 | } 606 | 607 | bootOptions := types.VirtualMachineBootOptions{ 608 | BootOrder: devices.BootOrder(order), 609 | } 610 | 611 | return vm.vm.SetBootOptions(vm.driver.ctx, &bootOptions) 612 | } 613 | 614 | func (vm *VirtualMachine) RemoveDevice(keepFiles bool, device ...types.BaseVirtualDevice) error { 615 | return vm.vm.RemoveDevice(vm.driver.ctx, keepFiles, device...) 616 | } 617 | 618 | func (vm *VirtualMachine) addDevice(device types.BaseVirtualDevice) error { 619 | newDevices := object.VirtualDeviceList{device} 620 | confSpec := types.VirtualMachineConfigSpec{} 621 | var err error 622 | confSpec.DeviceChange, err = newDevices.ConfigSpec(types.VirtualDeviceConfigSpecOperationAdd) 623 | if err != nil { 624 | return err 625 | } 626 | 627 | task, err := vm.vm.Reconfigure(vm.driver.ctx, confSpec) 628 | if err != nil { 629 | return err 630 | } 631 | 632 | _, err = task.WaitForResult(vm.driver.ctx, nil) 633 | return err 634 | } 635 | 636 | func (vm *VirtualMachine) AddConfigParams(params map[string]string) error { 637 | var confSpec types.VirtualMachineConfigSpec 638 | 639 | var ov []types.BaseOptionValue 640 | for k, v := range params { 641 | o := types.OptionValue{ 642 | Key: k, 643 | Value: v, 644 | } 645 | ov = append(ov, &o) 646 | } 647 | confSpec.ExtraConfig = ov 648 | 649 | task, err := vm.vm.Reconfigure(vm.driver.ctx, confSpec) 650 | if err != nil { 651 | return err 652 | } 653 | 654 | _, err = task.WaitForResult(vm.driver.ctx, nil) 655 | return err 656 | } 657 | 658 | func findNetworkAdapter(l object.VirtualDeviceList) (types.BaseVirtualEthernetCard, error) { 659 | c := l.SelectByType((*types.VirtualEthernetCard)(nil)) 660 | if len(c) == 0 { 661 | return nil, errors.New("no network adapter device found") 662 | } 663 | 664 | return c[0].(types.BaseVirtualEthernetCard), nil 665 | } 666 | -------------------------------------------------------------------------------- /driver/vm_cdrom.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import ( 4 | "errors" 5 | "github.com/vmware/govmomi/vim25/types" 6 | ) 7 | 8 | var ( 9 | ErrNoSataController = errors.New("no available SATA controller") 10 | ) 11 | 12 | func (vm *VirtualMachine) AddSATAController() error { 13 | sata := &types.VirtualAHCIController{} 14 | return vm.addDevice(sata) 15 | } 16 | 17 | func (vm *VirtualMachine) FindSATAController() (*types.VirtualAHCIController, error) { 18 | l, err := vm.Devices() 19 | if err != nil { 20 | return nil, err 21 | } 22 | 23 | c := l.PickController((*types.VirtualAHCIController)(nil)) 24 | if c == nil { 25 | return nil, ErrNoSataController 26 | } 27 | 28 | return c.(*types.VirtualAHCIController), nil 29 | } 30 | 31 | func (vm *VirtualMachine) CreateCdrom(c *types.VirtualController) (*types.VirtualCdrom, error) { 32 | l, err := vm.Devices() 33 | if err != nil { 34 | return nil, err 35 | } 36 | 37 | device := &types.VirtualCdrom{} 38 | 39 | l.AssignController(device, c) 40 | 41 | device.Backing = &types.VirtualCdromAtapiBackingInfo{ 42 | VirtualDeviceDeviceBackingInfo: types.VirtualDeviceDeviceBackingInfo{}, 43 | } 44 | 45 | device.Connectable = &types.VirtualDeviceConnectInfo{ 46 | AllowGuestControl: true, 47 | Connected: true, 48 | StartConnected: true, 49 | } 50 | 51 | return device, nil 52 | } 53 | 54 | func (vm *VirtualMachine) EjectCdroms() error { 55 | devices, err := vm.Devices() 56 | if err != nil { 57 | return err 58 | } 59 | cdroms := devices.SelectByType((*types.VirtualCdrom)(nil)) 60 | for _, cd := range cdroms { 61 | c := cd.(*types.VirtualCdrom) 62 | c.Backing = &types.VirtualCdromRemotePassthroughBackingInfo{} 63 | c.Connectable = &types.VirtualDeviceConnectInfo{} 64 | err := vm.vm.EditDevice(vm.driver.ctx, c) 65 | if err != nil { 66 | return err 67 | } 68 | } 69 | 70 | return nil 71 | } 72 | -------------------------------------------------------------------------------- /driver/vm_clone_acc_test.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "net" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func TestVMAcc_clone(t *testing.T) { 12 | testCases := []struct { 13 | name string 14 | config *CloneConfig 15 | checkFunction func(*testing.T, *VirtualMachine, *CloneConfig) 16 | }{ 17 | {"Default", &CloneConfig{}, cloneDefaultCheck}, 18 | {"LinkedClone", &CloneConfig{LinkedClone: true}, cloneLinkedCloneCheck}, 19 | {"Folder", &CloneConfig{LinkedClone: true, Folder: "folder1/folder2"}, cloneFolderCheck}, 20 | {"ResourcePool", &CloneConfig{LinkedClone: true, ResourcePool: "pool1/pool2"}, cloneResourcePoolCheck}, 21 | {"Configure", &CloneConfig{LinkedClone: true}, configureCheck}, 22 | {"Configure_RAMReserveAll", &CloneConfig{LinkedClone: true}, configureRAMReserveAllCheck}, 23 | {"StartAndStop", &CloneConfig{LinkedClone: true}, startAndStopCheck}, 24 | {"Template", &CloneConfig{LinkedClone: true}, templateCheck}, 25 | {"Snapshot", &CloneConfig{}, snapshotCheck}, 26 | } 27 | 28 | for _, tc := range testCases { 29 | t.Run(tc.name, func(t *testing.T) { 30 | tc.config.Host = TestHostName 31 | tc.config.Name = newVMName() 32 | 33 | templateName := "alpine" 34 | d := newTestDriver(t) 35 | 36 | template, err := d.FindVM(templateName) // Don't destroy this VM! 37 | if err != nil { 38 | t.Fatalf("Cannot find template vm '%v': %v", templateName, err) 39 | } 40 | 41 | log.Printf("[DEBUG] Clonning VM") 42 | vm, err := template.Clone(context.TODO(), tc.config) 43 | if err != nil { 44 | t.Fatalf("Cannot clone vm '%v': %v", templateName, err) 45 | } 46 | 47 | defer destroyVM(t, vm, tc.config.Name) 48 | 49 | log.Printf("[DEBUG] Running check function") 50 | tc.checkFunction(t, vm, tc.config) 51 | }) 52 | } 53 | } 54 | 55 | func cloneDefaultCheck(t *testing.T, vm *VirtualMachine, config *CloneConfig) { 56 | d := vm.driver 57 | 58 | // Check that the clone can be found by its name 59 | if _, err := d.FindVM(config.Name); err != nil { 60 | t.Errorf("Cannot find created vm '%v': %v", config.Name, err) 61 | } 62 | 63 | vmInfo, err := vm.Info("name", "parent", "runtime.host", "resourcePool", "datastore", "layoutEx.disk") 64 | if err != nil { 65 | t.Fatalf("Cannot read VM properties: %v", err) 66 | } 67 | 68 | if vmInfo.Name != config.Name { 69 | t.Errorf("Invalid VM name: expected '%v', got '%v'", config.Name, vmInfo.Name) 70 | } 71 | 72 | f := d.NewFolder(vmInfo.Parent) 73 | folderPath, err := f.Path() 74 | if err != nil { 75 | t.Fatalf("Cannot read folder name: %v", err) 76 | } 77 | if folderPath != "" { 78 | t.Errorf("Invalid folder: expected '/', got '%v'", folderPath) 79 | } 80 | 81 | h := d.NewHost(vmInfo.Runtime.Host) 82 | hostInfo, err := h.Info("name") 83 | if err != nil { 84 | t.Fatal("Cannot read host properties: ", err) 85 | } 86 | if hostInfo.Name != TestHostName { 87 | t.Errorf("Invalid host name: expected '%v', got '%v'", TestHostName, hostInfo.Name) 88 | } 89 | 90 | p := d.NewResourcePool(vmInfo.ResourcePool) 91 | poolPath, err := p.Path() 92 | if err != nil { 93 | t.Fatalf("Cannot read resource pool name: %v", err) 94 | } 95 | if poolPath != "" { 96 | t.Errorf("Invalid resource pool: expected '/', got '%v'", poolPath) 97 | } 98 | 99 | dsr := vmInfo.Datastore[0].Reference() 100 | ds := d.NewDatastore(&dsr) 101 | dsInfo, err := ds.Info("name") 102 | if err != nil { 103 | t.Fatal("Cannot read datastore properties: ", err) 104 | } 105 | if dsInfo.Name != "datastore1" { 106 | t.Errorf("Invalid datastore name: expected '%v', got '%v'", "datastore1", dsInfo.Name) 107 | } 108 | 109 | if len(vmInfo.LayoutEx.Disk[0].Chain) != 1 { 110 | t.Error("Not a full clone") 111 | } 112 | } 113 | 114 | func configureCheck(t *testing.T, vm *VirtualMachine, _ *CloneConfig) { 115 | log.Printf("[DEBUG] Configuring the vm") 116 | hwConfig := &HardwareConfig{ 117 | CPUs: 2, 118 | CPUReservation: 1000, 119 | CPULimit: 1500, 120 | RAM: 2048, 121 | RAMReservation: 1024, 122 | MemoryHotAddEnabled: true, 123 | CpuHotAddEnabled: true, 124 | } 125 | err := vm.Configure(hwConfig) 126 | if err != nil { 127 | t.Fatalf("Failed to configure VM: %v", err) 128 | } 129 | 130 | log.Printf("[DEBUG] Running checks") 131 | vmInfo, err := vm.Info("config") 132 | if err != nil { 133 | t.Fatalf("Cannot read VM properties: %v", err) 134 | } 135 | 136 | cpuSockets := vmInfo.Config.Hardware.NumCPU 137 | if cpuSockets != hwConfig.CPUs { 138 | t.Errorf("VM should have %v CPU sockets, got %v", hwConfig.CPUs, cpuSockets) 139 | } 140 | 141 | cpuReservation := *vmInfo.Config.CpuAllocation.Reservation 142 | if cpuReservation != hwConfig.CPUReservation { 143 | t.Errorf("VM should have CPU reservation for %v Mhz, got %v", hwConfig.CPUReservation, cpuReservation) 144 | } 145 | 146 | cpuLimit := *vmInfo.Config.CpuAllocation.Limit 147 | if cpuLimit != hwConfig.CPULimit { 148 | t.Errorf("VM should have CPU reservation for %v Mhz, got %v", hwConfig.CPULimit, cpuLimit) 149 | } 150 | 151 | ram := vmInfo.Config.Hardware.MemoryMB 152 | if int64(ram) != hwConfig.RAM { 153 | t.Errorf("VM should have %v MB of RAM, got %v", hwConfig.RAM, ram) 154 | } 155 | 156 | ramReservation := *vmInfo.Config.MemoryAllocation.Reservation 157 | if ramReservation != hwConfig.RAMReservation { 158 | t.Errorf("VM should have RAM reservation for %v MB, got %v", hwConfig.RAMReservation, ramReservation) 159 | } 160 | 161 | cpuHotAdd := vmInfo.Config.CpuHotAddEnabled 162 | if *cpuHotAdd != hwConfig.CpuHotAddEnabled { 163 | t.Errorf("VM should have CPU hot add set to %v, got %v", hwConfig.CpuHotAddEnabled, cpuHotAdd) 164 | } 165 | 166 | memoryHotAdd := vmInfo.Config.MemoryHotAddEnabled 167 | if *memoryHotAdd != hwConfig.MemoryHotAddEnabled { 168 | t.Errorf("VM should have Memroy hot add set to %v, got %v", hwConfig.MemoryHotAddEnabled, memoryHotAdd) 169 | } 170 | } 171 | 172 | func configureRAMReserveAllCheck(t *testing.T, vm *VirtualMachine, _ *CloneConfig) { 173 | log.Printf("[DEBUG] Configuring the vm") 174 | err := vm.Configure(&HardwareConfig{RAMReserveAll: true}) 175 | if err != nil { 176 | t.Fatalf("Failed to configure VM: %v", err) 177 | } 178 | 179 | log.Printf("[DEBUG] Running checks") 180 | vmInfo, err := vm.Info("config") 181 | if err != nil { 182 | t.Fatalf("Cannot read VM properties: %v", err) 183 | } 184 | 185 | if *vmInfo.Config.MemoryReservationLockedToMax != true { 186 | t.Errorf("VM should have all RAM reserved") 187 | } 188 | } 189 | 190 | func cloneLinkedCloneCheck(t *testing.T, vm *VirtualMachine, _ *CloneConfig) { 191 | vmInfo, err := vm.Info("layoutEx.disk") 192 | if err != nil { 193 | t.Fatalf("Cannot read VM properties: %v", err) 194 | } 195 | 196 | if len(vmInfo.LayoutEx.Disk[0].Chain) != 2 { 197 | t.Error("Not a linked clone") 198 | } 199 | } 200 | 201 | func cloneFolderCheck(t *testing.T, vm *VirtualMachine, config *CloneConfig) { 202 | vmInfo, err := vm.Info("parent") 203 | if err != nil { 204 | t.Fatalf("Cannot read VM properties: %v", err) 205 | } 206 | 207 | f := vm.driver.NewFolder(vmInfo.Parent) 208 | path, err := f.Path() 209 | if err != nil { 210 | t.Fatalf("Cannot read folder name: %v", err) 211 | } 212 | if path != config.Folder { 213 | t.Errorf("Wrong folder. expected: %v, got: %v", config.Folder, path) 214 | } 215 | } 216 | 217 | func cloneResourcePoolCheck(t *testing.T, vm *VirtualMachine, config *CloneConfig) { 218 | vmInfo, err := vm.Info("resourcePool") 219 | if err != nil { 220 | t.Fatalf("Cannot read VM properties: %v", err) 221 | } 222 | 223 | p := vm.driver.NewResourcePool(vmInfo.ResourcePool) 224 | path, err := p.Path() 225 | if err != nil { 226 | t.Fatalf("Cannot read resource pool name: %v", err) 227 | } 228 | if path != config.ResourcePool { 229 | t.Errorf("Wrong folder. expected: %v, got: %v", config.ResourcePool, path) 230 | } 231 | } 232 | 233 | func startAndStopCheck(t *testing.T, vm *VirtualMachine, config *CloneConfig) { 234 | stopper := startVM(t, vm, config.Name) 235 | defer stopper() 236 | 237 | switch ip, err := vm.WaitForIP(context.TODO()); { 238 | case err != nil: 239 | t.Errorf("Cannot obtain IP address from created vm '%v': %v", config.Name, err) 240 | case net.ParseIP(ip) == nil: 241 | t.Errorf("'%v' is not a valid ip address", ip) 242 | } 243 | 244 | err := vm.StartShutdown() 245 | if err != nil { 246 | t.Fatalf("Failed to initiate guest shutdown: %v", err) 247 | } 248 | log.Printf("[DEBUG] Waiting max 1m0s for shutdown to complete") 249 | err = vm.WaitForShutdown(context.TODO(), 1*time.Minute) 250 | if err != nil { 251 | t.Fatalf("Failed to wait for giest shutdown: %v", err) 252 | } 253 | } 254 | 255 | func snapshotCheck(t *testing.T, vm *VirtualMachine, config *CloneConfig) { 256 | stopper := startVM(t, vm, config.Name) 257 | defer stopper() 258 | 259 | err := vm.CreateSnapshot("test-snapshot") 260 | if err != nil { 261 | t.Fatalf("Failed to create snapshot: %v", err) 262 | } 263 | 264 | vmInfo, err := vm.Info("layoutEx.disk") 265 | if err != nil { 266 | t.Fatalf("Cannot read VM properties: %v", err) 267 | } 268 | 269 | layers := len(vmInfo.LayoutEx.Disk[0].Chain) 270 | if layers != 2 { 271 | t.Errorf("VM should have a single snapshot. expected 2 disk layers, got %v", layers) 272 | } 273 | } 274 | 275 | func templateCheck(t *testing.T, vm *VirtualMachine, _ *CloneConfig) { 276 | err := vm.ConvertToTemplate() 277 | if err != nil { 278 | t.Fatalf("Failed to convert to template: %v", err) 279 | } 280 | vmInfo, err := vm.Info("config.template") 281 | if err != nil { 282 | t.Errorf("Cannot read VM properties: %v", err) 283 | } else if !vmInfo.Config.Template { 284 | t.Error("Not a template") 285 | } 286 | } 287 | 288 | func startVM(t *testing.T, vm *VirtualMachine, vmName string) (stopper func()) { 289 | log.Printf("[DEBUG] Starting the vm") 290 | if err := vm.PowerOn(); err != nil { 291 | t.Fatalf("Cannot start vm '%v': %v", vmName, err) 292 | } 293 | return func() { 294 | log.Printf("[DEBUG] Powering off the vm") 295 | if err := vm.PowerOff(); err != nil { 296 | t.Errorf("Cannot power off started vm '%v': %v", vmName, err) 297 | } 298 | } 299 | } 300 | 301 | func destroyVM(t *testing.T, vm *VirtualMachine, vmName string) { 302 | log.Printf("[DEBUG] Deleting the VM") 303 | if err := vm.Destroy(); err != nil { 304 | t.Errorf("!!! ERROR DELETING VM '%v': %v!!!", vmName, err) 305 | } 306 | 307 | // Check that the clone is no longer exists 308 | if _, err := vm.driver.FindVM(vmName); err == nil { 309 | t.Errorf("!!! STILL CAN FIND VM '%v'. IT MIGHT NOT HAVE BEEN DELETED !!!", vmName) 310 | } 311 | } 312 | -------------------------------------------------------------------------------- /driver/vm_create_acc_test.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import ( 4 | "log" 5 | "testing" 6 | ) 7 | 8 | func TestVMAcc_create(t *testing.T) { 9 | testCases := []struct { 10 | name string 11 | config *CreateConfig 12 | checkFunction func(*testing.T, *VirtualMachine, *CreateConfig) 13 | }{ 14 | {"MinimalConfiguration", &CreateConfig{}, createDefaultCheck}, 15 | } 16 | 17 | for _, tc := range testCases { 18 | t.Run(tc.name, func(t *testing.T) { 19 | tc.config.Host = TestHostName 20 | tc.config.Name = newVMName() 21 | 22 | d := newTestDriver(t) 23 | 24 | log.Printf("[DEBUG] Creating VM") 25 | vm, err := d.CreateVM(tc.config) 26 | if err != nil { 27 | t.Fatalf("Cannot create VM: %v", err) 28 | } 29 | 30 | defer destroyVM(t, vm, tc.config.Name) 31 | 32 | log.Printf("[DEBUG] Running check function") 33 | tc.checkFunction(t, vm, tc.config) 34 | }) 35 | } 36 | } 37 | 38 | func createDefaultCheck(t *testing.T, vm *VirtualMachine, config *CreateConfig) { 39 | d := vm.driver 40 | 41 | // Check that the clone can be found by its name 42 | if _, err := d.FindVM(config.Name); err != nil { 43 | t.Errorf("Cannot find created vm '%v': %v", config.Name, err) 44 | } 45 | 46 | vmInfo, err := vm.Info("name", "parent", "runtime.host", "resourcePool", "datastore", "layoutEx.disk") 47 | if err != nil { 48 | t.Fatalf("Cannot read VM properties: %v", err) 49 | } 50 | 51 | if vmInfo.Name != config.Name { 52 | t.Errorf("Invalid VM name: expected '%v', got '%v'", config.Name, vmInfo.Name) 53 | } 54 | 55 | f := d.NewFolder(vmInfo.Parent) 56 | folderPath, err := f.Path() 57 | if err != nil { 58 | t.Fatalf("Cannot read folder name: %v", err) 59 | } 60 | if folderPath != "" { 61 | t.Errorf("Invalid folder: expected '/', got '%v'", folderPath) 62 | } 63 | 64 | h := d.NewHost(vmInfo.Runtime.Host) 65 | hostInfo, err := h.Info("name") 66 | if err != nil { 67 | t.Fatal("Cannot read host properties: ", err) 68 | } 69 | if hostInfo.Name != TestHostName { 70 | t.Errorf("Invalid host name: expected '%v', got '%v'", TestHostName, hostInfo.Name) 71 | } 72 | 73 | p := d.NewResourcePool(vmInfo.ResourcePool) 74 | poolPath, err := p.Path() 75 | if err != nil { 76 | t.Fatalf("Cannot read resource pool name: %v", err) 77 | } 78 | if poolPath != "" { 79 | t.Errorf("Invalid resource pool: expected '/', got '%v'", poolPath) 80 | } 81 | 82 | dsr := vmInfo.Datastore[0].Reference() 83 | ds := d.NewDatastore(&dsr) 84 | dsInfo, err := ds.Info("name") 85 | if err != nil { 86 | t.Fatal("Cannot read datastore properties: ", err) 87 | } 88 | if dsInfo.Name != "datastore1" { 89 | t.Errorf("Invalid datastore name: expected 'datastore1', got '%v'", dsInfo.Name) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /driver/vm_keyboard.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import ( 4 | "github.com/vmware/govmomi/vim25/methods" 5 | "github.com/vmware/govmomi/vim25/types" 6 | "golang.org/x/mobile/event/key" 7 | "strings" 8 | "unicode" 9 | ) 10 | 11 | type KeyInput struct { 12 | Message string 13 | Scancode key.Code 14 | Alt bool 15 | Ctrl bool 16 | Shift bool 17 | } 18 | 19 | var scancodeMap = make(map[rune]key.Code) 20 | 21 | func init() { 22 | scancodeIndex := make(map[string]key.Code) 23 | scancodeIndex["abcdefghijklmnopqrstuvwxyz"] = key.CodeA 24 | scancodeIndex["ABCDEFGHIJKLMNOPQRSTUVWXYZ"] = key.CodeA 25 | scancodeIndex["1234567890"] = key.Code1 26 | scancodeIndex["!@#$%^&*()"] = key.Code1 27 | scancodeIndex[" "] = key.CodeSpacebar 28 | scancodeIndex["-=[]\\"] = key.CodeHyphenMinus 29 | scancodeIndex["_+{}|"] = key.CodeHyphenMinus 30 | scancodeIndex[";'`,./"] = key.CodeSemicolon 31 | scancodeIndex[":\"~<>?"] = key.CodeSemicolon 32 | 33 | for chars, start := range scancodeIndex { 34 | for i, r := range chars { 35 | scancodeMap[r] = start + key.Code(i) 36 | } 37 | } 38 | } 39 | 40 | const shiftedChars = "!@#$%^&*()_+{}|:\"~<>?" 41 | 42 | func (vm *VirtualMachine) TypeOnKeyboard(input KeyInput) (int32, error) { 43 | var spec types.UsbScanCodeSpec 44 | 45 | for _, r := range input.Message { 46 | scancode := scancodeMap[r] 47 | shift := input.Shift || unicode.IsUpper(r) || strings.ContainsRune(shiftedChars, r) 48 | 49 | spec.KeyEvents = append(spec.KeyEvents, types.UsbScanCodeSpecKeyEvent{ 50 | // https://github.com/lamw/vghetto-scripts/blob/f74bc8ba20064f46592bcce5a873b161a7fa3d72/powershell/VMKeystrokes.ps1#L130 51 | UsbHidCode: int32(scancode)<<16 | 7, 52 | Modifiers: &types.UsbScanCodeSpecModifierType{ 53 | LeftControl: &input.Ctrl, 54 | LeftAlt: &input.Alt, 55 | LeftShift: &shift, 56 | }, 57 | }) 58 | } 59 | 60 | if input.Scancode != 0 { 61 | spec.KeyEvents = append(spec.KeyEvents, types.UsbScanCodeSpecKeyEvent{ 62 | UsbHidCode: int32(input.Scancode)<<16 | 7, 63 | Modifiers: &types.UsbScanCodeSpecModifierType{ 64 | LeftControl: &input.Ctrl, 65 | LeftAlt: &input.Alt, 66 | LeftShift: &input.Shift, 67 | }, 68 | }) 69 | } 70 | 71 | req := &types.PutUsbScanCodes{ 72 | This: vm.vm.Reference(), 73 | Spec: spec, 74 | } 75 | 76 | resp, err := methods.PutUsbScanCodes(vm.driver.ctx, vm.driver.client.RoundTripper, req) 77 | if err != nil { 78 | return 0, err 79 | } 80 | 81 | return resp.Returnval, nil 82 | } 83 | -------------------------------------------------------------------------------- /examples/alpine/alpine-3.8.json: -------------------------------------------------------------------------------- 1 | { 2 | "builders": [ 3 | { 4 | "type": "vsphere-iso", 5 | 6 | "vcenter_server": "vcenter.vsphere65.test", 7 | "username": "root", 8 | "password": "jetbrains", 9 | "insecure_connection": true, 10 | 11 | "vm_name": "alpine-{{timestamp}}", 12 | "host": "esxi-1.vsphere65.test", 13 | 14 | "CPUs": 1, 15 | "RAM": 512, 16 | "RAM_reserve_all": true, 17 | "disk_controller_type": "pvscsi", 18 | "disk_size": 1024, 19 | "disk_thin_provisioned": true, 20 | "network_card": "vmxnet3", 21 | 22 | "guest_os_type": "other3xLinux64Guest", 23 | 24 | "iso_paths": [ 25 | "[datastore1] ISO/alpine-standard-3.8.2-x86_64.iso" 26 | ], 27 | "floppy_files": [ 28 | "{{template_dir}}/answerfile", 29 | "{{template_dir}}/setup.sh" 30 | ], 31 | 32 | "boot_wait": "15s", 33 | "boot_command": [ 34 | "root", 35 | "mount -t vfat /dev/fd0 /media/floppy", 36 | "setup-alpine -f /media/floppy/answerfile", 37 | "", 38 | "jetbrains", 39 | "jetbrains", 40 | "", 41 | "y", 42 | "", 43 | "reboot", 44 | "", 45 | "root", 46 | "jetbrains", 47 | "mount -t vfat /dev/fd0 /media/floppy", 48 | "/media/floppy/SETUP.SH" 49 | ], 50 | 51 | "ssh_username": "root", 52 | "ssh_password": "jetbrains" 53 | } 54 | ], 55 | 56 | "provisioners": [ 57 | { 58 | "type": "shell", 59 | "inline": ["ls /"] 60 | } 61 | ] 62 | } 63 | -------------------------------------------------------------------------------- /examples/alpine/answerfile: -------------------------------------------------------------------------------- 1 | KEYMAPOPTS="us us" 2 | HOSTNAMEOPTS="-n alpine" 3 | INTERFACESOPTS="auto lo 4 | iface lo inet loopback 5 | 6 | auto eth0 7 | iface eth0 inet dhcp 8 | hostname alpine 9 | " 10 | TIMEZONEOPTS="-z UTC" 11 | PROXYOPTS="none" 12 | APKREPOSOPTS="http://mirror.yandex.ru/mirrors/alpine/v3.8/main" 13 | SSHDOPTS="-c openssh" 14 | NTPOPTS="-c none" 15 | DISKOPTS="-m sys /dev/sda" 16 | -------------------------------------------------------------------------------- /examples/alpine/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ex 4 | 5 | apk add libressl 6 | apk add open-vm-tools 7 | rc-update add open-vm-tools 8 | /etc/init.d/open-vm-tools start 9 | 10 | cat >/usr/local/bin/shutdown <", 43 | "ut", 44 | "/Volumes/setup/setup.sh" 45 | ], 46 | 47 | "ssh_username": "jetbrains", 48 | "ssh_password": "jetbrains" 49 | } 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /examples/macos/setup/.gitignore: -------------------------------------------------------------------------------- 1 | out/ 2 | -------------------------------------------------------------------------------- /examples/macos/setup/iso-macos.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -eux 3 | 4 | # Based on 5 | # https://gist.github.com/agentsim/00cc38c693e7d0e1b36a2080870d955b#gistcomment-2304505 6 | 7 | mkdir -p out 8 | 9 | hdiutil create -o out/HighSierra.cdr -size 5530m -layout SPUD -fs HFS+J 10 | hdiutil attach out/HighSierra.cdr.dmg -noverify -mountpoint /Volumes/install_build 11 | sudo /Applications/Install\ macOS\ High\ Sierra.app/Contents/Resources/createinstallmedia --volume /Volumes/install_build --nointeraction 12 | hdiutil detach /Volumes/Install\ macOS\ High\ Sierra 13 | hdiutil convert out/HighSierra.cdr.dmg -format UDTO -o out/HighSierra.iso 14 | mv out/HighSierra.iso.cdr out/HighSierra.iso 15 | rm out/HighSierra.cdr.dmg 16 | -------------------------------------------------------------------------------- /examples/macos/setup/iso-setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -eux 3 | 4 | mkdir -p out/pkgroot 5 | rm -rf /out/pkgroot/* 6 | 7 | mkdir -p out/scripts 8 | rm -rf /out/scripts/* 9 | cp postinstall out/scripts/ 10 | 11 | pkgbuild \ 12 | --identifier io.packer.install \ 13 | --root out/pkgroot \ 14 | --scripts out/scripts \ 15 | out/postinstall.pkg 16 | 17 | mkdir -p out/iso 18 | rm -rf out/iso/* 19 | cp setup.sh out/iso/ 20 | chmod +x out/iso/setup.sh 21 | 22 | productbuild --package out/postinstall.pkg out/iso/postinstall.pkg 23 | 24 | rm -f out/setup.iso 25 | hdiutil makehybrid -iso -joliet -default-volume-name setup -o out/setup.iso out/iso 26 | cd out 27 | shasum -a 256 setup.iso >sha256sums 28 | -------------------------------------------------------------------------------- /examples/macos/setup/postinstall: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -eux 3 | # debug output in /var/log/install.log 4 | 5 | # Create user account 6 | USERNAME=jetbrains 7 | PASSWORD=jetbrains 8 | dscl . -create "/Users/${USERNAME}" 9 | dscl . -create "/Users/${USERNAME}" UserShell /bin/bash 10 | dscl . -create "/Users/${USERNAME}" RealName "${USERNAME}" 11 | dscl . -create "/Users/${USERNAME}" UniqueID 510 12 | dscl . -create "/Users/${USERNAME}" PrimaryGroupID 20 13 | dscl . -create "/Users/${USERNAME}" NFSHomeDirectory "/Users/${USERNAME}" 14 | dscl . -passwd "/Users/${USERNAME}" "${PASSWORD}" 15 | dscl . -append /Groups/admin GroupMembership "${USERNAME}" 16 | createhomedir -c 17 | 18 | # Start VMware Tools daemon explicitly 19 | launchctl load /Library/LaunchDaemons/com.vmware.launchd.tools.plist 20 | 21 | # Enable SSH 22 | systemsetup -setremotelogin on 23 | 24 | # Disable the welcome screen 25 | touch "$3/private/var/db/.AppleSetupDone" 26 | -------------------------------------------------------------------------------- /examples/macos/setup/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -eux 3 | 4 | # Format partition 5 | diskutil eraseDisk JHFS+ Disk disk0 6 | 7 | # Packages are installed in reversed order - why? 8 | "/Volumes/Image Volume/Install macOS High Sierra.app/Contents/Resources/startosinstall" \ 9 | --volume /Volumes/Disk \ 10 | --converttoapfs no \ 11 | --agreetolicense \ 12 | --installpackage "/Volumes/setup/postinstall.pkg" \ 13 | --installpackage "/Volumes/VMware Tools/Install VMware Tools.app/Contents/Resources/VMware Tools.pkg" 14 | -------------------------------------------------------------------------------- /examples/ubuntu/preseed.cfg: -------------------------------------------------------------------------------- 1 | d-i passwd/user-fullname string jetbrains 2 | d-i passwd/username string jetbrains 3 | d-i passwd/user-password password jetbrains 4 | d-i passwd/user-password-again password jetbrains 5 | d-i user-setup/allow-password-weak boolean true 6 | 7 | d-i partman-auto/disk string /dev/sda 8 | d-i partman-auto/method string regular 9 | d-i partman-partitioning/confirm_write_new_label boolean true 10 | d-i partman/choose_partition select finish 11 | d-i partman/confirm boolean true 12 | d-i partman/confirm_nooverwrite boolean true 13 | 14 | d-i pkgsel/include string open-vm-tools openssh-server 15 | 16 | d-i finish-install/reboot_in_progress note 17 | -------------------------------------------------------------------------------- /examples/ubuntu/ubuntu-16.04.json: -------------------------------------------------------------------------------- 1 | { 2 | "builders": [ 3 | { 4 | "type": "vsphere-iso", 5 | 6 | "vcenter_server": "vcenter.vsphere65.test", 7 | "username": "root", 8 | "password": "jetbrains", 9 | "insecure_connection": "true", 10 | 11 | "vm_name": "example-ubuntu", 12 | "host": "esxi-1.vsphere65.test", 13 | 14 | "guest_os_type": "ubuntu64Guest", 15 | 16 | "ssh_username": "jetbrains", 17 | "ssh_password": "jetbrains", 18 | 19 | "CPUs": 1, 20 | "RAM": 1024, 21 | "RAM_reserve_all": true, 22 | 23 | "disk_controller_type": "pvscsi", 24 | "disk_size": 32768, 25 | "disk_thin_provisioned": true, 26 | 27 | "network_card": "vmxnet3", 28 | 29 | "iso_paths": [ 30 | "[datastore1] ISO/ubuntu-16.04.3-server-amd64.iso" 31 | ], 32 | "floppy_files": [ 33 | "{{template_dir}}/preseed.cfg" 34 | ], 35 | "boot_command": [ 36 | "", 37 | "", 38 | "", 39 | "", 40 | "", 41 | "", 42 | "", 43 | "", 44 | "", 45 | "", 46 | "/install/vmlinuz", 47 | " initrd=/install/initrd.gz", 48 | " priority=critical", 49 | " locale=en_US", 50 | " file=/media/preseed.cfg", 51 | "" 52 | ] 53 | } 54 | ], 55 | 56 | "provisioners": [ 57 | { 58 | "type": "shell", 59 | "inline": ["ls /"] 60 | } 61 | ] 62 | } 63 | -------------------------------------------------------------------------------- /examples/windows/.gitattributes: -------------------------------------------------------------------------------- 1 | *.cmd text eol=crlf 2 | *.ps1 text eol=crlf 3 | -------------------------------------------------------------------------------- /examples/windows/setup/Autounattend.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | en-US 6 | 7 | 8 | 9 | 10 | 11 | 12 | B:\ 13 | 14 | 15 | 16 | 17 | 18 | 19 | true 20 | 21 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | /IMAGE/NAME 33 | Windows 10 Pro 34 | 35 | 36 | true 37 | 38 | 39 | 40 | 41 | 42 | 0 43 | 44 | 45 | 1 46 | true 47 | Primary 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | false 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 1 67 | 68 | a:\vmtools.cmd 69 | Always 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | en-US 78 | 79 | 80 | 81 | 82 | 83 | 3 84 | 85 | 86 | 89 | 90 | 91 | 92 | 93 | jetbrains 94 | 95 | jetbrains 96 | true</PlainText> 97 | </Password> 98 | <Group>Administrators</Group> 99 | </LocalAccount> 100 | </LocalAccounts> 101 | </UserAccounts> 102 | 103 | <AutoLogon> 104 | <Enabled>true</Enabled> 105 | <Username>jetbrains</Username> 106 | <Password> 107 | <Value>jetbrains</Value> 108 | <PlainText>true</PlainText> 109 | </Password> 110 | <LogonCount>1</LogonCount> 111 | </AutoLogon> 112 | <FirstLogonCommands> 113 | <SynchronousCommand wcm:action="add"> 114 | <Order>1</Order> 115 | <!-- Enable WinRM service --> 116 | <CommandLine>powershell -ExecutionPolicy Bypass -File a:\setup.ps1</CommandLine> 117 | <RequiresUserInput>true</RequiresUserInput> 118 | </SynchronousCommand> 119 | </FirstLogonCommands> 120 | </component> 121 | </settings> 122 | </unattend> 123 | -------------------------------------------------------------------------------- /examples/windows/setup/setup.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = "Stop" 2 | 3 | # Switch network connection to private mode 4 | # Required for WinRM firewall rules 5 | $profile = Get-NetConnectionProfile 6 | Set-NetConnectionProfile -Name $profile.Name -NetworkCategory Private 7 | 8 | # Enable WinRM service 9 | winrm quickconfig -quiet 10 | winrm set winrm/config/service '@{AllowUnencrypted="true"}' 11 | winrm set winrm/config/service/auth '@{Basic="true"}' 12 | 13 | # Reset auto logon count 14 | # https://docs.microsoft.com/en-us/windows-hardware/customize/desktop/unattend/microsoft-windows-shell-setup-autologon-logoncount#logoncount-known-issue 15 | Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' -Name AutoLogonCount -Value 0 16 | -------------------------------------------------------------------------------- /examples/windows/setup/vmtools.cmd: -------------------------------------------------------------------------------- 1 | @rem Silent mode, basic UI, no reboot 2 | e:\setup64 /s /v "/qb REBOOT=R" 3 | -------------------------------------------------------------------------------- /examples/windows/windows-10.json: -------------------------------------------------------------------------------- 1 | { 2 | "builders": [ 3 | { 4 | "type": "vsphere-iso", 5 | 6 | "vcenter_server": "vcenter.vsphere65.test", 7 | "username": "root", 8 | "password": "jetbrains", 9 | "insecure_connection": "true", 10 | 11 | "vm_name": "example-windows", 12 | "host": "esxi-1.vsphere65.test", 13 | 14 | "guest_os_type": "windows9_64Guest", 15 | 16 | "communicator": "winrm", 17 | "winrm_username": "jetbrains", 18 | "winrm_password": "jetbrains", 19 | 20 | "CPUs": 1, 21 | "RAM": 4096, 22 | "RAM_reserve_all": true, 23 | 24 | "disk_controller_type": "pvscsi", 25 | "disk_size": 32768, 26 | "disk_thin_provisioned": true, 27 | 28 | "network_card": "vmxnet3", 29 | 30 | "iso_paths": [ 31 | "[datastore1] ISO/en_windows_10_multi-edition_vl_version_1709_updated_dec_2017_x64_dvd_100406172.iso", 32 | "[datastore1] ISO/VMware Tools/10.2.0/windows.iso" 33 | ], 34 | 35 | "floppy_files": [ 36 | "{{template_dir}}/setup/" 37 | ], 38 | "floppy_img_path": "[datastore1] ISO/VMware Tools/10.2.0/pvscsi-Windows8.flp" 39 | } 40 | ], 41 | 42 | "provisioners": [ 43 | { 44 | "type": "windows-shell", 45 | "inline": ["dir c:\\"] 46 | } 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jetbrains-infra/packer-builder-vsphere 2 | 3 | require ( 4 | github.com/hashicorp/packer v1.4.2 5 | github.com/vmware/govmomi v0.20.0 6 | go.uber.org/goleak v0.10.1-0.20190517053103-3b0196519f16 7 | golang.org/x/mobile v0.0.0-20190607214518-6fa95d984e88 8 | ) 9 | 10 | replace git.apache.org/thrift.git => github.com/apache/thrift v0.0.0-20180902110319-2566ecd5d999 11 | 12 | -------------------------------------------------------------------------------- /gofmt.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | RETVAL=0 4 | 5 | for file in $(find . -name '*.go' -not -path './build/*') 6 | do 7 | if [ -n "$(gofmt -l $file)" ] 8 | then 9 | echo "$file does not conform to gofmt rules. Run: gofmt -s -w $file" >&2 10 | RETVAL=1 11 | fi 12 | done 13 | 14 | exit $RETVAL 15 | -------------------------------------------------------------------------------- /iso/builder.go: -------------------------------------------------------------------------------- 1 | package iso 2 | 3 | import ( 4 | "context" 5 | packerCommon "github.com/hashicorp/packer/common" 6 | "github.com/hashicorp/packer/helper/communicator" 7 | "github.com/hashicorp/packer/helper/multistep" 8 | "github.com/hashicorp/packer/packer" 9 | "github.com/jetbrains-infra/packer-builder-vsphere/common" 10 | "github.com/jetbrains-infra/packer-builder-vsphere/driver" 11 | ) 12 | 13 | type Builder struct { 14 | config *Config 15 | runner multistep.Runner 16 | } 17 | 18 | func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { 19 | c, warnings, errs := NewConfig(raws...) 20 | if errs != nil { 21 | return warnings, errs 22 | } 23 | b.config = c 24 | 25 | return warnings, nil 26 | } 27 | 28 | func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (packer.Artifact, error) { 29 | state := new(multistep.BasicStateBag) 30 | state.Put("comm", &b.config.Comm) 31 | state.Put("hook", hook) 32 | state.Put("ui", ui) 33 | 34 | var steps []multistep.Step 35 | 36 | steps = append(steps, 37 | &common.StepConnect{ 38 | Config: &b.config.ConnectConfig, 39 | }, 40 | ) 41 | 42 | if b.config.ISOUrls != nil { 43 | steps = append(steps, 44 | &packerCommon.StepDownload{ 45 | Checksum: b.config.ISOChecksum, 46 | ChecksumType: b.config.ISOChecksumType, 47 | Description: "ISO", 48 | Extension: b.config.TargetExtension, 49 | ResultKey: "iso_path", 50 | TargetPath: b.config.TargetPath, 51 | Url: b.config.ISOUrls, 52 | }, 53 | &StepRemoteUpload{ 54 | Datastore: b.config.Datastore, 55 | Host: b.config.Host, 56 | }, 57 | ) 58 | } 59 | 60 | steps = append(steps, 61 | &StepCreateVM{ 62 | Config: &b.config.CreateConfig, 63 | Location: &b.config.LocationConfig, 64 | Force: b.config.PackerConfig.PackerForce, 65 | }, 66 | &common.StepConfigureHardware{ 67 | Config: &b.config.HardwareConfig, 68 | }, 69 | &StepAddCDRom{ 70 | Config: &b.config.CDRomConfig, 71 | }, 72 | &common.StepConfigParams{ 73 | Config: &b.config.ConfigParamsConfig, 74 | }, 75 | ) 76 | 77 | if b.config.Comm.Type != "none" { 78 | steps = append(steps, 79 | &packerCommon.StepCreateFloppy{ 80 | Files: b.config.FloppyFiles, 81 | Directories: b.config.FloppyDirectories, 82 | }, 83 | &StepAddFloppy{ 84 | Config: &b.config.FloppyConfig, 85 | Datastore: b.config.Datastore, 86 | Host: b.config.Host, 87 | }, 88 | &packerCommon.StepHTTPServer{ 89 | HTTPDir: b.config.HTTPDir, 90 | HTTPPortMin: b.config.HTTPPortMin, 91 | HTTPPortMax: b.config.HTTPPortMax, 92 | }, 93 | &common.StepRun{ 94 | Config: &b.config.RunConfig, 95 | SetOrder: true, 96 | }, 97 | &StepBootCommand{ 98 | Config: &b.config.BootConfig, 99 | Ctx: b.config.ctx, 100 | VMName: b.config.VMName, 101 | }, 102 | &common.StepWaitForIp{ 103 | Config: &b.config.WaitIpConfig, 104 | }, 105 | &communicator.StepConnect{ 106 | Config: &b.config.Comm, 107 | Host: common.CommHost(b.config.Comm.SSHHost), 108 | SSHConfig: b.config.Comm.SSHConfigFunc(), 109 | }, 110 | &packerCommon.StepProvision{}, 111 | &common.StepShutdown{ 112 | Config: &b.config.ShutdownConfig, 113 | }, 114 | &StepRemoveFloppy{ 115 | Datastore: b.config.Datastore, 116 | Host: b.config.Host, 117 | }, 118 | ) 119 | } 120 | 121 | steps = append(steps, 122 | &StepRemoveCDRom{}, 123 | &common.StepCreateSnapshot{ 124 | CreateSnapshot: b.config.CreateSnapshot, 125 | }, 126 | &common.StepConvertToTemplate{ 127 | ConvertToTemplate: b.config.ConvertToTemplate, 128 | }, 129 | ) 130 | 131 | b.runner = packerCommon.NewRunner(steps, b.config.PackerConfig, ui) 132 | b.runner.Run(ctx, state) 133 | 134 | if rawErr, ok := state.GetOk("error"); ok { 135 | return nil, rawErr.(error) 136 | } 137 | 138 | if _, ok := state.GetOk("vm"); !ok { 139 | return nil, nil 140 | } 141 | artifact := &common.Artifact{ 142 | Name: b.config.VMName, 143 | VM: state.Get("vm").(*driver.VirtualMachine), 144 | } 145 | return artifact, nil 146 | } 147 | -------------------------------------------------------------------------------- /iso/builder_acc_test.go: -------------------------------------------------------------------------------- 1 | package iso 2 | 3 | import ( 4 | "fmt" 5 | builderT "github.com/hashicorp/packer/helper/builder/testing" 6 | "github.com/hashicorp/packer/packer" 7 | commonT "github.com/jetbrains-infra/packer-builder-vsphere/common/testing" 8 | "github.com/vmware/govmomi/vim25/types" 9 | "io/ioutil" 10 | "os" 11 | "testing" 12 | ) 13 | 14 | func TestISOBuilderAcc_default(t *testing.T) { 15 | config := defaultConfig() 16 | builderT.Test(t, builderT.TestCase{ 17 | Builder: &Builder{}, 18 | Template: commonT.RenderConfig(config), 19 | Check: checkDefault(t, config["vm_name"].(string), config["host"].(string), "datastore1"), 20 | }) 21 | } 22 | 23 | func defaultConfig() map[string]interface{} { 24 | username := os.Getenv("VSPHERE_USERNAME") 25 | if username == "" { 26 | username = "root" 27 | } 28 | password := os.Getenv("VSPHERE_PASSWORD") 29 | if password == "" { 30 | password = "jetbrains" 31 | } 32 | 33 | config := map[string]interface{}{ 34 | "vcenter_server": "vcenter.vsphere65.test", 35 | "username": username, 36 | "password": password, 37 | "insecure_connection": true, 38 | 39 | "host": "esxi-1.vsphere65.test", 40 | 41 | "ssh_username": "root", 42 | "ssh_password": "jetbrains", 43 | 44 | "vm_name": commonT.NewVMName(), 45 | "disk_size": 2048, 46 | 47 | "communicator": "none", // do not start the VM without any bootable devices 48 | } 49 | 50 | return config 51 | } 52 | 53 | func checkDefault(t *testing.T, name string, host string, datastore string) builderT.TestCheckFunc { 54 | return func(artifacts []packer.Artifact) error { 55 | d := commonT.TestConn(t) 56 | vm := commonT.GetVM(t, d, artifacts) 57 | 58 | vmInfo, err := vm.Info("name", "parent", "runtime.host", "resourcePool", "datastore", "layoutEx.disk", "config.firmware") 59 | if err != nil { 60 | t.Fatalf("Cannot read VM properties: %v", err) 61 | } 62 | 63 | if vmInfo.Name != name { 64 | t.Errorf("Invalid VM name: expected '%v', got '%v'", name, vmInfo.Name) 65 | } 66 | 67 | f := d.NewFolder(vmInfo.Parent) 68 | folderPath, err := f.Path() 69 | if err != nil { 70 | t.Fatalf("Cannot read folder name: %v", err) 71 | } 72 | if folderPath != "" { 73 | t.Errorf("Invalid folder: expected '/', got '%v'", folderPath) 74 | } 75 | 76 | h := d.NewHost(vmInfo.Runtime.Host) 77 | hostInfo, err := h.Info("name") 78 | if err != nil { 79 | t.Fatal("Cannot read host properties: ", err) 80 | } 81 | if hostInfo.Name != host { 82 | t.Errorf("Invalid host name: expected '%v', got '%v'", host, hostInfo.Name) 83 | } 84 | 85 | p := d.NewResourcePool(vmInfo.ResourcePool) 86 | poolPath, err := p.Path() 87 | if err != nil { 88 | t.Fatalf("Cannot read resource pool name: %v", err) 89 | } 90 | if poolPath != "" { 91 | t.Errorf("Invalid resource pool: expected '/', got '%v'", poolPath) 92 | } 93 | 94 | dsr := vmInfo.Datastore[0].Reference() 95 | ds := d.NewDatastore(&dsr) 96 | dsInfo, err := ds.Info("name") 97 | if err != nil { 98 | t.Fatal("Cannot read datastore properties: ", err) 99 | } 100 | if dsInfo.Name != datastore { 101 | t.Errorf("Invalid datastore name: expected '%v', got '%v'", datastore, dsInfo.Name) 102 | } 103 | 104 | fw := vmInfo.Config.Firmware 105 | if fw != "bios" { 106 | t.Errorf("Invalid firmware: expected 'bios', got '%v'", fw) 107 | } 108 | 109 | return nil 110 | } 111 | } 112 | 113 | func TestISOBuilderAcc_notes(t *testing.T) { 114 | builderT.Test(t, builderT.TestCase{ 115 | Builder: &Builder{}, 116 | Template: notesConfig(), 117 | Check: checkNotes(t), 118 | }) 119 | } 120 | 121 | func notesConfig() string { 122 | config := defaultConfig() 123 | config["notes"] = "test" 124 | 125 | return commonT.RenderConfig(config) 126 | } 127 | 128 | func checkNotes(t *testing.T) builderT.TestCheckFunc { 129 | return func(artifacts []packer.Artifact) error { 130 | d := commonT.TestConn(t) 131 | vm := commonT.GetVM(t, d, artifacts) 132 | 133 | vmInfo, err := vm.Info("config.annotation") 134 | if err != nil { 135 | t.Fatalf("Cannot read VM properties: %v", err) 136 | } 137 | 138 | notes := vmInfo.Config.Annotation 139 | if notes != "test" { 140 | t.Errorf("notes should be 'test'") 141 | } 142 | 143 | return nil 144 | } 145 | } 146 | 147 | func TestISOBuilderAcc_hardware(t *testing.T) { 148 | builderT.Test(t, builderT.TestCase{ 149 | Builder: &Builder{}, 150 | Template: hardwareConfig(), 151 | Check: checkHardware(t), 152 | }) 153 | } 154 | 155 | func hardwareConfig() string { 156 | config := defaultConfig() 157 | config["CPUs"] = 2 158 | config["cpu_cores"] = 2 159 | config["CPU_reservation"] = 1000 160 | config["CPU_limit"] = 1500 161 | config["RAM"] = 2048 162 | config["RAM_reservation"] = 1024 163 | config["NestedHV"] = true 164 | config["firmware"] = "efi" 165 | config["video_ram"] = 8192 166 | 167 | return commonT.RenderConfig(config) 168 | } 169 | 170 | func checkHardware(t *testing.T) builderT.TestCheckFunc { 171 | return func(artifacts []packer.Artifact) error { 172 | d := commonT.TestConn(t) 173 | 174 | vm := commonT.GetVM(t, d, artifacts) 175 | vmInfo, err := vm.Info("config") 176 | if err != nil { 177 | t.Fatalf("Cannot read VM properties: %v", err) 178 | } 179 | 180 | cpuSockets := vmInfo.Config.Hardware.NumCPU 181 | if cpuSockets != 2 { 182 | t.Errorf("VM should have 2 CPU sockets, got %v", cpuSockets) 183 | } 184 | 185 | cpuCores := vmInfo.Config.Hardware.NumCoresPerSocket 186 | if cpuCores != 2 { 187 | t.Errorf("VM should have 2 CPU cores per socket, got %v", cpuCores) 188 | } 189 | 190 | cpuReservation := *vmInfo.Config.CpuAllocation.Reservation 191 | if cpuReservation != 1000 { 192 | t.Errorf("VM should have CPU reservation for 1000 Mhz, got %v", cpuReservation) 193 | } 194 | 195 | cpuLimit := *vmInfo.Config.CpuAllocation.Limit 196 | if cpuLimit != 1500 { 197 | t.Errorf("VM should have CPU reservation for 1500 Mhz, got %v", cpuLimit) 198 | } 199 | 200 | ram := vmInfo.Config.Hardware.MemoryMB 201 | if ram != 2048 { 202 | t.Errorf("VM should have 2048 MB of RAM, got %v", ram) 203 | } 204 | 205 | ramReservation := *vmInfo.Config.MemoryAllocation.Reservation 206 | if ramReservation != 1024 { 207 | t.Errorf("VM should have RAM reservation for 1024 MB, got %v", ramReservation) 208 | } 209 | 210 | nestedHV := vmInfo.Config.NestedHVEnabled 211 | if !*nestedHV { 212 | t.Errorf("VM should have NestedHV enabled, got %v", nestedHV) 213 | } 214 | 215 | fw := vmInfo.Config.Firmware 216 | if fw != "efi" { 217 | t.Errorf("Invalid firmware: expected 'efi', got '%v'", fw) 218 | } 219 | 220 | l, err := vm.Devices() 221 | if err != nil { 222 | t.Fatalf("Cannot read VM devices: %v", err) 223 | } 224 | c := l.PickController((*types.VirtualIDEController)(nil)) 225 | if c == nil { 226 | t.Errorf("VM should have IDE controller") 227 | } 228 | s := l.PickController((*types.VirtualAHCIController)(nil)) 229 | if s != nil { 230 | t.Errorf("VM should have no SATA controllers") 231 | } 232 | 233 | v := l.SelectByType((*types.VirtualMachineVideoCard)(nil)) 234 | if len(v) != 1 { 235 | t.Errorf("VM should have one video card") 236 | } 237 | if v[0].(*types.VirtualMachineVideoCard).VideoRamSizeInKB != 8192 { 238 | t.Errorf("Video RAM should be equal 8192") 239 | } 240 | 241 | return nil 242 | } 243 | } 244 | 245 | func TestISOBuilderAcc_limit(t *testing.T) { 246 | builderT.Test(t, builderT.TestCase{ 247 | Builder: &Builder{}, 248 | Template: limitConfig(), 249 | Check: checkLimit(t), 250 | }) 251 | } 252 | 253 | func limitConfig() string { 254 | config := defaultConfig() 255 | config["CPUs"] = 1 // hardware is customized, but CPU limit is not specified explicitly 256 | 257 | return commonT.RenderConfig(config) 258 | } 259 | 260 | func checkLimit(t *testing.T) builderT.TestCheckFunc { 261 | return func(artifacts []packer.Artifact) error { 262 | d := commonT.TestConn(t) 263 | 264 | vm := commonT.GetVM(t, d, artifacts) 265 | vmInfo, err := vm.Info("config.cpuAllocation") 266 | if err != nil { 267 | t.Fatalf("Cannot read VM properties: %v", err) 268 | } 269 | 270 | limit := *vmInfo.Config.CpuAllocation.Limit 271 | if limit != -1 { // must be unlimited 272 | t.Errorf("Invalid CPU limit: expected '%v', got '%v'", -1, limit) 273 | } 274 | 275 | return nil 276 | } 277 | } 278 | 279 | func TestISOBuilderAcc_sata(t *testing.T) { 280 | builderT.Test(t, builderT.TestCase{ 281 | Builder: &Builder{}, 282 | Template: sataConfig(), 283 | Check: checkSata(t), 284 | }) 285 | } 286 | 287 | func sataConfig() string { 288 | config := defaultConfig() 289 | config["cdrom_type"] = "sata" 290 | 291 | return commonT.RenderConfig(config) 292 | } 293 | 294 | func checkSata(t *testing.T) builderT.TestCheckFunc { 295 | return func(artifacts []packer.Artifact) error { 296 | d := commonT.TestConn(t) 297 | 298 | vm := commonT.GetVM(t, d, artifacts) 299 | 300 | l, err := vm.Devices() 301 | if err != nil { 302 | t.Fatalf("Cannot read VM devices: %v", err) 303 | } 304 | 305 | c := l.PickController((*types.VirtualAHCIController)(nil)) 306 | if c == nil { 307 | t.Errorf("VM has no SATA controllers") 308 | } 309 | 310 | return nil 311 | } 312 | } 313 | 314 | func TestISOBuilderAcc_cdrom(t *testing.T) { 315 | builderT.Test(t, builderT.TestCase{ 316 | Builder: &Builder{}, 317 | Template: cdromConfig(), 318 | }) 319 | } 320 | 321 | func cdromConfig() string { 322 | config := defaultConfig() 323 | config["iso_paths"] = []string{ 324 | "[datastore1] test0.iso", 325 | "[datastore1] test1.iso", 326 | } 327 | return commonT.RenderConfig(config) 328 | } 329 | 330 | func TestISOBuilderAcc_networkCard(t *testing.T) { 331 | builderT.Test(t, builderT.TestCase{ 332 | Builder: &Builder{}, 333 | Template: networkCardConfig(), 334 | Check: checkNetworkCard(t), 335 | }) 336 | } 337 | 338 | func networkCardConfig() string { 339 | config := defaultConfig() 340 | config["network_card"] = "vmxnet3" 341 | return commonT.RenderConfig(config) 342 | } 343 | 344 | func checkNetworkCard(t *testing.T) builderT.TestCheckFunc { 345 | return func(artifacts []packer.Artifact) error { 346 | d := commonT.TestConn(t) 347 | 348 | vm := commonT.GetVM(t, d, artifacts) 349 | devices, err := vm.Devices() 350 | if err != nil { 351 | t.Fatalf("Cannot read VM properties: %v", err) 352 | } 353 | 354 | netCards := devices.SelectByType((*types.VirtualEthernetCard)(nil)) 355 | if len(netCards) == 0 { 356 | t.Fatalf("Cannot find the network card") 357 | } 358 | if len(netCards) > 1 { 359 | t.Fatalf("Found several network catds") 360 | } 361 | if _, ok := netCards[0].(*types.VirtualVmxnet3); !ok { 362 | t.Errorf("The network card type is not the expected one (vmxnet3)") 363 | } 364 | 365 | return nil 366 | } 367 | } 368 | 369 | func TestISOBuilderAcc_createFloppy(t *testing.T) { 370 | tmpFile, err := ioutil.TempFile("", "packer-vsphere-iso-test") 371 | if err != nil { 372 | t.Fatalf("Error creating temp file: %v", err) 373 | } 374 | _, err = fmt.Fprint(tmpFile, "Hello, World!") 375 | if err != nil { 376 | t.Fatalf("Error creating temp file: %v", err) 377 | } 378 | err = tmpFile.Close() 379 | if err != nil { 380 | t.Fatalf("Error creating temp file: %v", err) 381 | } 382 | 383 | builderT.Test(t, builderT.TestCase{ 384 | Builder: &Builder{}, 385 | Template: createFloppyConfig(tmpFile.Name()), 386 | }) 387 | } 388 | 389 | func createFloppyConfig(filePath string) string { 390 | config := defaultConfig() 391 | config["floppy_files"] = []string{filePath} 392 | return commonT.RenderConfig(config) 393 | } 394 | 395 | func TestISOBuilderAcc_full(t *testing.T) { 396 | config := fullConfig() 397 | builderT.Test(t, builderT.TestCase{ 398 | Builder: &Builder{}, 399 | Template: commonT.RenderConfig(config), 400 | Check: checkFull(t), 401 | }) 402 | } 403 | 404 | func fullConfig() map[string]interface{} { 405 | username := os.Getenv("VSPHERE_USERNAME") 406 | if username == "" { 407 | username = "root" 408 | } 409 | password := os.Getenv("VSPHERE_PASSWORD") 410 | if password == "" { 411 | password = "jetbrains" 412 | } 413 | 414 | config := map[string]interface{}{ 415 | "vcenter_server": "vcenter.vsphere65.test", 416 | "username": username, 417 | "password": password, 418 | "insecure_connection": true, 419 | 420 | "vm_name": commonT.NewVMName(), 421 | "host": "esxi-1.vsphere65.test", 422 | 423 | "RAM": 512, 424 | "disk_controller_type": "pvscsi", 425 | "disk_size": 1024, 426 | "disk_thin_provisioned": true, 427 | "network_card": "vmxnet3", 428 | "guest_os_type": "other3xLinux64Guest", 429 | 430 | "iso_paths": []string{ 431 | "[datastore1] ISO/alpine-standard-3.8.2-x86_64.iso", 432 | }, 433 | "floppy_files": []string{ 434 | "../examples/alpine/answerfile", 435 | "../examples/alpine/setup.sh", 436 | }, 437 | 438 | "boot_wait": "20s", 439 | "boot_command": []string{ 440 | "root<enter><wait>", 441 | "mount -t vfat /dev/fd0 /media/floppy<enter><wait>", 442 | "setup-alpine -f /media/floppy/answerfile<enter>", 443 | "<wait5>", 444 | "jetbrains<enter>", 445 | "jetbrains<enter>", 446 | "<wait5>", 447 | "y<enter>", 448 | "<wait10><wait10><wait10><wait10>", 449 | "reboot<enter>", 450 | "<wait10><wait10><wait10>", 451 | "root<enter>", 452 | "jetbrains<enter><wait>", 453 | "mount -t vfat /dev/fd0 /media/floppy<enter><wait>", 454 | "/media/floppy/SETUP.SH<enter>", 455 | }, 456 | 457 | "ssh_username": "root", 458 | "ssh_password": "jetbrains", 459 | } 460 | 461 | return config 462 | } 463 | 464 | func checkFull(t *testing.T) builderT.TestCheckFunc { 465 | return func(artifacts []packer.Artifact) error { 466 | d := commonT.TestConn(t) 467 | vm := commonT.GetVM(t, d, artifacts) 468 | 469 | vmInfo, err := vm.Info("config.bootOptions") 470 | if err != nil { 471 | t.Fatalf("Cannot read VM properties: %v", err) 472 | } 473 | 474 | order := vmInfo.Config.BootOptions.BootOrder 475 | if order != nil { 476 | t.Errorf("Boot order must be empty") 477 | } 478 | 479 | devices, err := vm.Devices() 480 | if err != nil { 481 | t.Fatalf("Cannot read devices: %v", err) 482 | } 483 | cdroms := devices.SelectByType((*types.VirtualCdrom)(nil)) 484 | for _, cd := range cdroms { 485 | _, ok := cd.(*types.VirtualCdrom).Backing.(*types.VirtualCdromRemotePassthroughBackingInfo) 486 | if !ok { 487 | t.Errorf("wrong cdrom backing") 488 | } 489 | } 490 | 491 | return nil 492 | } 493 | } 494 | 495 | func TestISOBuilderAcc_bootOrder(t *testing.T) { 496 | config := fullConfig() 497 | config["boot_order"] = "disk,cdrom,floppy" 498 | 499 | builderT.Test(t, builderT.TestCase{ 500 | Builder: &Builder{}, 501 | Template: commonT.RenderConfig(config), 502 | Check: checkBootOrder(t), 503 | }) 504 | } 505 | 506 | func checkBootOrder(t *testing.T) builderT.TestCheckFunc { 507 | return func(artifacts []packer.Artifact) error { 508 | d := commonT.TestConn(t) 509 | vm := commonT.GetVM(t, d, artifacts) 510 | 511 | vmInfo, err := vm.Info("config.bootOptions") 512 | if err != nil { 513 | t.Fatalf("Cannot read VM properties: %v", err) 514 | } 515 | 516 | order := vmInfo.Config.BootOptions.BootOrder 517 | if order == nil { 518 | t.Errorf("Boot order must not be empty") 519 | } 520 | 521 | return nil 522 | } 523 | } 524 | 525 | func TestISOBuilderAcc_cluster(t *testing.T) { 526 | builderT.Test(t, builderT.TestCase{ 527 | Builder: &Builder{}, 528 | Template: clusterConfig(), 529 | }) 530 | } 531 | 532 | func clusterConfig() string { 533 | config := defaultConfig() 534 | config["cluster"] = "cluster1" 535 | config["host"] = "esxi-2.vsphere65.test" 536 | 537 | return commonT.RenderConfig(config) 538 | } 539 | 540 | func TestISOBuilderAcc_clusterDRS(t *testing.T) { 541 | builderT.Test(t, builderT.TestCase{ 542 | Builder: &Builder{}, 543 | Template: clusterDRSConfig(), 544 | }) 545 | } 546 | 547 | func clusterDRSConfig() string { 548 | config := defaultConfig() 549 | config["cluster"] = "cluster2" 550 | config["host"] = "" 551 | config["datastore"] = "datastore3" // bug #183 552 | config["network"] = "VM Network" // bug #183 553 | 554 | return commonT.RenderConfig(config) 555 | } 556 | -------------------------------------------------------------------------------- /iso/config.go: -------------------------------------------------------------------------------- 1 | package iso 2 | 3 | import ( 4 | packerCommon "github.com/hashicorp/packer/common" 5 | "github.com/hashicorp/packer/helper/communicator" 6 | "github.com/hashicorp/packer/helper/config" 7 | "github.com/hashicorp/packer/packer" 8 | "github.com/hashicorp/packer/template/interpolate" 9 | "github.com/jetbrains-infra/packer-builder-vsphere/common" 10 | ) 11 | 12 | type Config struct { 13 | packerCommon.PackerConfig `mapstructure:",squash"` 14 | packerCommon.HTTPConfig `mapstructure:",squash"` 15 | 16 | common.ConnectConfig `mapstructure:",squash"` 17 | CreateConfig `mapstructure:",squash"` 18 | common.LocationConfig `mapstructure:",squash"` 19 | common.HardwareConfig `mapstructure:",squash"` 20 | common.ConfigParamsConfig `mapstructure:",squash"` 21 | 22 | packerCommon.ISOConfig `mapstructure:",squash"` 23 | 24 | CDRomConfig `mapstructure:",squash"` 25 | FloppyConfig `mapstructure:",squash"` 26 | common.RunConfig `mapstructure:",squash"` 27 | BootConfig `mapstructure:",squash"` 28 | common.WaitIpConfig `mapstructure:",squash"` 29 | Comm communicator.Config `mapstructure:",squash"` 30 | 31 | common.ShutdownConfig `mapstructure:",squash"` 32 | 33 | CreateSnapshot bool `mapstructure:"create_snapshot"` 34 | ConvertToTemplate bool `mapstructure:"convert_to_template"` 35 | 36 | ctx interpolate.Context 37 | } 38 | 39 | func NewConfig(raws ...interface{}) (*Config, []string, error) { 40 | c := new(Config) 41 | err := config.Decode(c, &config.DecodeOpts{ 42 | Interpolate: true, 43 | InterpolateContext: &c.ctx, 44 | InterpolateFilter: &interpolate.RenderFilter{ 45 | Exclude: []string{ 46 | "boot_command", 47 | }, 48 | }, 49 | }, raws...) 50 | if err != nil { 51 | return nil, nil, err 52 | } 53 | 54 | warnings := make([]string, 0) 55 | errs := new(packer.MultiError) 56 | 57 | if c.ISOUrls != nil { 58 | isoWarnings, isoErrs := c.ISOConfig.Prepare(&c.ctx) 59 | warnings = append(warnings, isoWarnings...) 60 | errs = packer.MultiErrorAppend(errs, isoErrs...) 61 | } 62 | 63 | errs = packer.MultiErrorAppend(errs, c.ConnectConfig.Prepare()...) 64 | errs = packer.MultiErrorAppend(errs, c.CreateConfig.Prepare()...) 65 | errs = packer.MultiErrorAppend(errs, c.LocationConfig.Prepare()...) 66 | errs = packer.MultiErrorAppend(errs, c.HardwareConfig.Prepare()...) 67 | errs = packer.MultiErrorAppend(errs, c.HTTPConfig.Prepare(&c.ctx)...) 68 | 69 | errs = packer.MultiErrorAppend(errs, c.CDRomConfig.Prepare()...) 70 | errs = packer.MultiErrorAppend(errs, c.BootConfig.Prepare()...) 71 | errs = packer.MultiErrorAppend(errs, c.WaitIpConfig.Prepare()...) 72 | errs = packer.MultiErrorAppend(errs, c.Comm.Prepare(&c.ctx)...) 73 | errs = packer.MultiErrorAppend(errs, c.ShutdownConfig.Prepare()...) 74 | 75 | if len(errs.Errors) > 0 { 76 | return nil, nil, errs 77 | } 78 | 79 | return c, nil, nil 80 | } 81 | -------------------------------------------------------------------------------- /iso/leak_test.go: -------------------------------------------------------------------------------- 1 | package iso 2 | 3 | import "testing" 4 | import "go.uber.org/goleak" 5 | 6 | func TestMain(m *testing.M) { 7 | goleak.VerifyTestMain(m, goleak.IgnoreTopFunction("go.opencensus.io/stats/view.(*worker).start")) 8 | } 9 | -------------------------------------------------------------------------------- /iso/step_add_cdrom.go: -------------------------------------------------------------------------------- 1 | package iso 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/hashicorp/packer/helper/multistep" 7 | "github.com/hashicorp/packer/packer" 8 | "github.com/jetbrains-infra/packer-builder-vsphere/driver" 9 | ) 10 | 11 | type CDRomConfig struct { 12 | CdromType string `mapstructure:"cdrom_type"` 13 | ISOPaths []string `mapstructure:"iso_paths"` 14 | } 15 | 16 | type StepAddCDRom struct { 17 | Config *CDRomConfig 18 | } 19 | 20 | func (c *CDRomConfig) Prepare() []error { 21 | var errs []error 22 | 23 | if c.CdromType != "" && c.CdromType != "ide" && c.CdromType != "sata" { 24 | errs = append(errs, fmt.Errorf("'cdrom_type' must be 'ide' or 'sata'")) 25 | } 26 | 27 | return errs 28 | } 29 | 30 | func (s *StepAddCDRom) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { 31 | ui := state.Get("ui").(packer.Ui) 32 | vm := state.Get("vm").(*driver.VirtualMachine) 33 | 34 | if s.Config.CdromType == "sata" { 35 | if _, err := vm.FindSATAController(); err == driver.ErrNoSataController { 36 | ui.Say("Adding SATA controller...") 37 | if err := vm.AddSATAController(); err != nil { 38 | state.Put("error", fmt.Errorf("error adding SATA controller: %v", err)) 39 | return multistep.ActionHalt 40 | } 41 | } 42 | } 43 | 44 | ui.Say("Mount ISO images...") 45 | if len(s.Config.ISOPaths) > 0 { 46 | for _, path := range s.Config.ISOPaths { 47 | if err := vm.AddCdrom(s.Config.CdromType, path); err != nil { 48 | state.Put("error", fmt.Errorf("error mounting an image '%v': %v", path, err)) 49 | return multistep.ActionHalt 50 | } 51 | } 52 | } 53 | 54 | if path, ok := state.GetOk("iso_remote_path"); ok { 55 | if err := vm.AddCdrom(s.Config.CdromType, path.(string)); err != nil { 56 | state.Put("error", fmt.Errorf("error mounting an image '%v': %v", path, err)) 57 | return multistep.ActionHalt 58 | } 59 | } 60 | return multistep.ActionContinue 61 | } 62 | 63 | func (s *StepAddCDRom) Cleanup(state multistep.StateBag) {} 64 | -------------------------------------------------------------------------------- /iso/step_add_floppy.go: -------------------------------------------------------------------------------- 1 | package iso 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/hashicorp/packer/helper/multistep" 7 | "github.com/hashicorp/packer/packer" 8 | "github.com/jetbrains-infra/packer-builder-vsphere/driver" 9 | ) 10 | 11 | type FloppyConfig struct { 12 | FloppyIMGPath string `mapstructure:"floppy_img_path"` 13 | FloppyFiles []string `mapstructure:"floppy_files"` 14 | FloppyDirectories []string `mapstructure:"floppy_dirs"` 15 | } 16 | 17 | type StepAddFloppy struct { 18 | Config *FloppyConfig 19 | Datastore string 20 | Host string 21 | } 22 | 23 | func (s *StepAddFloppy) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { 24 | ui := state.Get("ui").(packer.Ui) 25 | vm := state.Get("vm").(*driver.VirtualMachine) 26 | d := state.Get("driver").(*driver.Driver) 27 | 28 | if floppyPath, ok := state.GetOk("floppy_path"); ok { 29 | ui.Say("Uploading created floppy image") 30 | 31 | ds, err := d.FindDatastore(s.Datastore, s.Host) 32 | if err != nil { 33 | state.Put("error", err) 34 | return multistep.ActionHalt 35 | } 36 | vmDir, err := vm.GetDir() 37 | if err != nil { 38 | state.Put("error", err) 39 | return multistep.ActionHalt 40 | } 41 | 42 | uploadPath := fmt.Sprintf("%v/packer-tmp-created-floppy.flp", vmDir) 43 | if err := ds.UploadFile(floppyPath.(string), uploadPath, s.Host); err != nil { 44 | state.Put("error", err) 45 | return multistep.ActionHalt 46 | } 47 | state.Put("uploaded_floppy_path", uploadPath) 48 | 49 | ui.Say("Adding generated Floppy...") 50 | floppyIMGPath := ds.ResolvePath(uploadPath) 51 | err = vm.AddFloppy(floppyIMGPath) 52 | if err != nil { 53 | state.Put("error", err) 54 | return multistep.ActionHalt 55 | } 56 | } 57 | 58 | if s.Config.FloppyIMGPath != "" { 59 | ui.Say("Adding Floppy image...") 60 | err := vm.AddFloppy(s.Config.FloppyIMGPath) 61 | if err != nil { 62 | state.Put("error", err) 63 | return multistep.ActionHalt 64 | } 65 | } 66 | 67 | return multistep.ActionContinue 68 | } 69 | 70 | func (s *StepAddFloppy) Cleanup(state multistep.StateBag) { 71 | _, cancelled := state.GetOk(multistep.StateCancelled) 72 | _, halted := state.GetOk(multistep.StateHalted) 73 | if !cancelled && !halted { 74 | return 75 | } 76 | 77 | ui := state.Get("ui").(packer.Ui) 78 | d := state.Get("driver").(*driver.Driver) 79 | 80 | if UploadedFloppyPath, ok := state.GetOk("uploaded_floppy_path"); ok { 81 | ui.Say("Deleting Floppy image ...") 82 | 83 | ds, err := d.FindDatastore(s.Datastore, s.Host) 84 | if err != nil { 85 | state.Put("error", err) 86 | return 87 | } 88 | 89 | err = ds.Delete(UploadedFloppyPath.(string)) 90 | if err != nil { 91 | state.Put("error", err) 92 | return 93 | } 94 | 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /iso/step_boot_command.go: -------------------------------------------------------------------------------- 1 | package iso 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | packerCommon "github.com/hashicorp/packer/common" 7 | "github.com/hashicorp/packer/helper/multistep" 8 | "github.com/hashicorp/packer/packer" 9 | "github.com/hashicorp/packer/template/interpolate" 10 | "github.com/jetbrains-infra/packer-builder-vsphere/driver" 11 | "golang.org/x/mobile/event/key" 12 | "log" 13 | "net" 14 | "os" 15 | "strings" 16 | "time" 17 | "unicode/utf8" 18 | ) 19 | 20 | type BootConfig struct { 21 | BootCommand []string `mapstructure:"boot_command"` 22 | BootWait time.Duration `mapstructure:"boot_wait"` // example: "1m30s"; default: "10s" 23 | HTTPIP string `mapstructure:"http_ip"` 24 | } 25 | 26 | type bootCommandTemplateData struct { 27 | HTTPIP string 28 | HTTPPort int 29 | Name string 30 | } 31 | 32 | func (c *BootConfig) Prepare() []error { 33 | var errs []error 34 | 35 | if c.BootWait == 0 { 36 | c.BootWait = 10 * time.Second 37 | } 38 | 39 | return errs 40 | } 41 | 42 | type StepBootCommand struct { 43 | Config *BootConfig 44 | VMName string 45 | Ctx interpolate.Context 46 | } 47 | 48 | var special = map[string]key.Code{ 49 | "<enter>": key.CodeReturnEnter, 50 | "<esc>": key.CodeEscape, 51 | "<bs>": key.CodeDeleteBackspace, 52 | "<del>": key.CodeDeleteForward, 53 | "<tab>": key.CodeTab, 54 | "<f1>": key.CodeF1, 55 | "<f2>": key.CodeF2, 56 | "<f3>": key.CodeF3, 57 | "<f4>": key.CodeF4, 58 | "<f5>": key.CodeF5, 59 | "<f6>": key.CodeF6, 60 | "<f7>": key.CodeF7, 61 | "<f8>": key.CodeF8, 62 | "<f9>": key.CodeF9, 63 | "<f10>": key.CodeF10, 64 | "<f11>": key.CodeF11, 65 | "<f12>": key.CodeF12, 66 | "<insert>": key.CodeInsert, 67 | "<home>": key.CodeHome, 68 | "<end>": key.CodeEnd, 69 | "<pageUp>": key.CodePageUp, 70 | "<pageDown>": key.CodePageDown, 71 | "<left>": key.CodeLeftArrow, 72 | "<right>": key.CodeRightArrow, 73 | "<up>": key.CodeUpArrow, 74 | "<down>": key.CodeDownArrow, 75 | } 76 | 77 | var keyInterval = packerCommon.PackerKeyDefault 78 | 79 | func init() { 80 | if delay, err := time.ParseDuration(os.Getenv(packerCommon.PackerKeyEnv)); err == nil { 81 | keyInterval = delay 82 | } 83 | } 84 | 85 | func (s *StepBootCommand) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { 86 | ui := state.Get("ui").(packer.Ui) 87 | vm := state.Get("vm").(*driver.VirtualMachine) 88 | 89 | if s.Config.BootCommand == nil { 90 | return multistep.ActionContinue 91 | } 92 | 93 | ui.Say(fmt.Sprintf("Waiting %s for boot...", s.Config.BootWait)) 94 | wait := time.After(s.Config.BootWait) 95 | WAITLOOP: 96 | for { 97 | select { 98 | case <-wait: 99 | break WAITLOOP 100 | case <-time.After(1 * time.Second): 101 | if _, ok := state.GetOk(multistep.StateCancelled); ok { 102 | return multistep.ActionHalt 103 | } 104 | } 105 | } 106 | 107 | port := state.Get("http_port").(int) 108 | if port > 0 { 109 | ip, err := getHostIP(s.Config.HTTPIP) 110 | if err != nil { 111 | state.Put("error", err) 112 | return multistep.ActionHalt 113 | } 114 | err = packerCommon.SetHTTPIP(ip) 115 | if err != nil { 116 | state.Put("error", err) 117 | return multistep.ActionHalt 118 | } 119 | 120 | s.Ctx.Data = &bootCommandTemplateData{ 121 | ip, 122 | port, 123 | s.VMName, 124 | } 125 | ui.Say(fmt.Sprintf("HTTP server is working at http://%v:%v/", ip, port)) 126 | } 127 | 128 | ui.Say("Typing boot command...") 129 | var keyAlt bool 130 | var keyCtrl bool 131 | var keyShift bool 132 | for _, command := range s.Config.BootCommand { 133 | message, err := interpolate.Render(command, &s.Ctx) 134 | if err != nil { 135 | state.Put("error", err) 136 | return multistep.ActionHalt 137 | } 138 | 139 | for len(message) > 0 { 140 | if _, ok := state.GetOk(multistep.StateCancelled); ok { 141 | return multistep.ActionHalt 142 | } 143 | 144 | if strings.HasPrefix(message, "<wait>") { 145 | log.Printf("Waiting 1 second") 146 | time.Sleep(1 * time.Second) 147 | message = message[len("<wait>"):] 148 | continue 149 | } 150 | 151 | if strings.HasPrefix(message, "<wait5>") { 152 | log.Printf("Waiting 5 seconds") 153 | time.Sleep(5 * time.Second) 154 | message = message[len("<wait5>"):] 155 | continue 156 | } 157 | 158 | if strings.HasPrefix(message, "<wait10>") { 159 | log.Printf("Waiting 10 seconds") 160 | time.Sleep(10 * time.Second) 161 | message = message[len("<wait10>"):] 162 | continue 163 | } 164 | 165 | if strings.HasPrefix(message, "<leftAltOn>") { 166 | keyAlt = true 167 | message = message[len("<leftAltOn>"):] 168 | continue 169 | } 170 | 171 | if strings.HasPrefix(message, "<leftAltOff>") { 172 | keyAlt = false 173 | message = message[len("<leftAltOff>"):] 174 | continue 175 | } 176 | 177 | if strings.HasPrefix(message, "<leftCtrlOn>") { 178 | keyCtrl = true 179 | message = message[len("<leftCtrlOn>"):] 180 | continue 181 | } 182 | 183 | if strings.HasPrefix(message, "<leftCtrlOff>") { 184 | keyCtrl = false 185 | message = message[len("<leftCtrlOff>"):] 186 | continue 187 | } 188 | 189 | if strings.HasPrefix(message, "<leftShiftOn>") { 190 | keyShift = true 191 | message = message[len("<leftShiftOn>"):] 192 | continue 193 | } 194 | 195 | if strings.HasPrefix(message, "<leftShiftOff>") { 196 | keyShift = false 197 | message = message[len("<leftShiftOff>"):] 198 | continue 199 | } 200 | 201 | var scancode key.Code 202 | for specialCode, specialValue := range special { 203 | if strings.HasPrefix(message, specialCode) { 204 | scancode = specialValue 205 | log.Printf("Special code '%s' found, replacing with: %s", specialCode, specialValue) 206 | message = message[len(specialCode):] 207 | } 208 | } 209 | 210 | var char rune 211 | if scancode == 0 { 212 | var size int 213 | char, size = utf8.DecodeRuneInString(message) 214 | message = message[size:] 215 | } 216 | 217 | _, err := vm.TypeOnKeyboard(driver.KeyInput{ 218 | Message: string(char), 219 | Scancode: scancode, 220 | Ctrl: keyCtrl, 221 | Alt: keyAlt, 222 | Shift: keyShift, 223 | }) 224 | if err != nil { 225 | state.Put("error", fmt.Errorf("error typing a boot command: %v", err)) 226 | return multistep.ActionHalt 227 | } 228 | time.Sleep(keyInterval) 229 | } 230 | } 231 | 232 | return multistep.ActionContinue 233 | } 234 | 235 | func (s *StepBootCommand) Cleanup(state multistep.StateBag) {} 236 | 237 | func getHostIP(s string) (string, error) { 238 | if s != "" { 239 | if net.ParseIP(s) != nil { 240 | return s, nil 241 | } else { 242 | return "", fmt.Errorf("invalid IP address") 243 | } 244 | } 245 | 246 | addrs, err := net.InterfaceAddrs() 247 | if err != nil { 248 | return "", err 249 | } 250 | 251 | for _, a := range addrs { 252 | ipnet, ok := a.(*net.IPNet) 253 | if ok && !ipnet.IP.IsLoopback() { 254 | if ipnet.IP.To4() != nil { 255 | return ipnet.IP.String(), nil 256 | } 257 | } 258 | } 259 | return "", fmt.Errorf("IP not found") 260 | } 261 | -------------------------------------------------------------------------------- /iso/step_create.go: -------------------------------------------------------------------------------- 1 | package iso 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/hashicorp/packer/helper/multistep" 7 | "github.com/hashicorp/packer/packer" 8 | "github.com/jetbrains-infra/packer-builder-vsphere/common" 9 | "github.com/jetbrains-infra/packer-builder-vsphere/driver" 10 | ) 11 | 12 | type CreateConfig struct { 13 | Version uint `mapstructure:"vm_version"` 14 | GuestOSType string `mapstructure:"guest_os_type"` 15 | Firmware string `mapstructure:"firmware"` 16 | 17 | DiskControllerType string `mapstructure:"disk_controller_type"` 18 | DiskSize int64 `mapstructure:"disk_size"` 19 | DiskThinProvisioned bool `mapstructure:"disk_thin_provisioned"` 20 | 21 | Network string `mapstructure:"network"` 22 | NetworkCard string `mapstructure:"network_card"` 23 | USBController bool `mapstructure:"usb_controller"` 24 | 25 | Notes string `mapstructure:"notes"` 26 | } 27 | 28 | func (c *CreateConfig) Prepare() []error { 29 | var errs []error 30 | 31 | if c.DiskSize == 0 { 32 | errs = append(errs, fmt.Errorf("'disk_size' is required")) 33 | } 34 | 35 | if c.GuestOSType == "" { 36 | c.GuestOSType = "otherGuest" 37 | } 38 | 39 | if c.Firmware != "" && c.Firmware != "bios" && c.Firmware != "efi" { 40 | errs = append(errs, fmt.Errorf("'firmware' must be 'bios' or 'efi'")) 41 | } 42 | 43 | return errs 44 | } 45 | 46 | type StepCreateVM struct { 47 | Config *CreateConfig 48 | Location *common.LocationConfig 49 | Force bool 50 | } 51 | 52 | func (s *StepCreateVM) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { 53 | ui := state.Get("ui").(packer.Ui) 54 | d := state.Get("driver").(*driver.Driver) 55 | 56 | vm, err := d.FindVM(s.Location.VMName) 57 | 58 | if s.Force == false && err == nil { 59 | state.Put("error", fmt.Errorf("%s already exists, you can use -force flag to destroy it: %v", s.Location.VMName, err)) 60 | return multistep.ActionHalt 61 | } else if s.Force == true && err == nil { 62 | ui.Say(fmt.Sprintf("the vm/template %s already exists, but deleting it due to -force flag", s.Location.VMName)) 63 | err := vm.Destroy() 64 | if err != nil { 65 | state.Put("error", fmt.Errorf("error destroying %s: %v", s.Location.VMName, err)) 66 | } 67 | } 68 | 69 | ui.Say("Creating VM...") 70 | vm, err = d.CreateVM(&driver.CreateConfig{ 71 | DiskThinProvisioned: s.Config.DiskThinProvisioned, 72 | DiskControllerType: s.Config.DiskControllerType, 73 | DiskSize: s.Config.DiskSize, 74 | Name: s.Location.VMName, 75 | Folder: s.Location.Folder, 76 | Cluster: s.Location.Cluster, 77 | Host: s.Location.Host, 78 | ResourcePool: s.Location.ResourcePool, 79 | Datastore: s.Location.Datastore, 80 | GuestOS: s.Config.GuestOSType, 81 | Network: s.Config.Network, 82 | NetworkCard: s.Config.NetworkCard, 83 | USBController: s.Config.USBController, 84 | Version: s.Config.Version, 85 | Firmware: s.Config.Firmware, 86 | Annotation: s.Config.Notes, 87 | }) 88 | if err != nil { 89 | state.Put("error", fmt.Errorf("error creating vm: %v", err)) 90 | return multistep.ActionHalt 91 | } 92 | state.Put("vm", vm) 93 | 94 | return multistep.ActionContinue 95 | } 96 | 97 | func (s *StepCreateVM) Cleanup(state multistep.StateBag) { 98 | _, cancelled := state.GetOk(multistep.StateCancelled) 99 | _, halted := state.GetOk(multistep.StateHalted) 100 | if !cancelled && !halted { 101 | return 102 | } 103 | 104 | ui := state.Get("ui").(packer.Ui) 105 | 106 | st := state.Get("vm") 107 | if st == nil { 108 | return 109 | } 110 | vm := st.(*driver.VirtualMachine) 111 | 112 | ui.Say("Destroying VM...") 113 | err := vm.Destroy() 114 | if err != nil { 115 | ui.Error(err.Error()) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /iso/step_remote_upload.go: -------------------------------------------------------------------------------- 1 | package iso 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/hashicorp/packer/helper/multistep" 7 | "github.com/hashicorp/packer/packer" 8 | "github.com/jetbrains-infra/packer-builder-vsphere/driver" 9 | "path/filepath" 10 | ) 11 | 12 | type StepRemoteUpload struct { 13 | Datastore string 14 | Host string 15 | } 16 | 17 | func (s *StepRemoteUpload) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { 18 | ui := state.Get("ui").(packer.Ui) 19 | d := state.Get("driver").(*driver.Driver) 20 | 21 | if path, ok := state.GetOk("iso_path"); ok { 22 | filename := filepath.Base(path.(string)) 23 | 24 | ds, err := d.FindDatastore(s.Datastore, s.Host) 25 | if err != nil { 26 | state.Put("error", fmt.Errorf("datastore doesn't exist: %v", err)) 27 | return multistep.ActionHalt 28 | } 29 | 30 | remotePath := fmt.Sprintf("packer_cache/%s", filename) 31 | remoteDirectory := fmt.Sprintf("[%s] packer_cache/", ds.Name()) 32 | fullRemotePath := fmt.Sprintf("%s/%s", remoteDirectory, filename) 33 | 34 | ui.Say(fmt.Sprintf("Uploading %s to %s", filename, remotePath)) 35 | 36 | if exists := ds.FileExists(remotePath); exists == true { 37 | ui.Say("File already upload") 38 | state.Put("iso_remote_path", fullRemotePath) 39 | return multistep.ActionContinue 40 | } 41 | 42 | if err := ds.MakeDirectory(remoteDirectory); err != nil { 43 | state.Put("error", err) 44 | return multistep.ActionHalt 45 | } 46 | 47 | if err := ds.UploadFile(path.(string), remotePath, s.Host); err != nil { 48 | state.Put("error", err) 49 | return multistep.ActionHalt 50 | } 51 | state.Put("iso_remote_path", fullRemotePath) 52 | } 53 | 54 | return multistep.ActionContinue 55 | } 56 | 57 | func (s *StepRemoteUpload) Cleanup(state multistep.StateBag) {} 58 | -------------------------------------------------------------------------------- /iso/step_remove_cdrom.go: -------------------------------------------------------------------------------- 1 | package iso 2 | 3 | import ( 4 | "context" 5 | "github.com/hashicorp/packer/helper/multistep" 6 | "github.com/hashicorp/packer/packer" 7 | "github.com/jetbrains-infra/packer-builder-vsphere/driver" 8 | ) 9 | 10 | type StepRemoveCDRom struct{} 11 | 12 | func (s *StepRemoveCDRom) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { 13 | ui := state.Get("ui").(packer.Ui) 14 | vm := state.Get("vm").(*driver.VirtualMachine) 15 | 16 | ui.Say("Eject CD-ROM drives...") 17 | err := vm.EjectCdroms() 18 | if err != nil { 19 | state.Put("error", err) 20 | return multistep.ActionHalt 21 | } 22 | 23 | return multistep.ActionContinue 24 | } 25 | 26 | func (s *StepRemoveCDRom) Cleanup(state multistep.StateBag) {} 27 | -------------------------------------------------------------------------------- /iso/step_remove_floppy.go: -------------------------------------------------------------------------------- 1 | package iso 2 | 3 | import ( 4 | "context" 5 | "github.com/hashicorp/packer/helper/multistep" 6 | "github.com/hashicorp/packer/packer" 7 | "github.com/jetbrains-infra/packer-builder-vsphere/driver" 8 | "github.com/vmware/govmomi/vim25/types" 9 | ) 10 | 11 | type StepRemoveFloppy struct { 12 | Datastore string 13 | Host string 14 | } 15 | 16 | func (s *StepRemoveFloppy) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { 17 | ui := state.Get("ui").(packer.Ui) 18 | vm := state.Get("vm").(*driver.VirtualMachine) 19 | d := state.Get("driver").(*driver.Driver) 20 | 21 | ui.Say("Deleting Floppy drives...") 22 | devices, err := vm.Devices() 23 | if err != nil { 24 | state.Put("error", err) 25 | return multistep.ActionHalt 26 | } 27 | floppies := devices.SelectByType((*types.VirtualFloppy)(nil)) 28 | if err = vm.RemoveDevice(true, floppies...); err != nil { 29 | state.Put("error", err) 30 | return multistep.ActionHalt 31 | } 32 | 33 | if UploadedFloppyPath, ok := state.GetOk("uploaded_floppy_path"); ok { 34 | ui.Say("Deleting Floppy image...") 35 | ds, err := d.FindDatastore(s.Datastore, s.Host) 36 | if err != nil { 37 | state.Put("error", err) 38 | return multistep.ActionHalt 39 | } 40 | if err := ds.Delete(UploadedFloppyPath.(string)); err != nil { 41 | state.Put("error", err) 42 | return multistep.ActionHalt 43 | } 44 | } 45 | 46 | return multistep.ActionContinue 47 | } 48 | 49 | func (s *StepRemoveFloppy) Cleanup(state multistep.StateBag) {} 50 | -------------------------------------------------------------------------------- /teamcity-services.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | vpn: 4 | container_name: vpn 5 | image: jetbrainsinfra/openvpn 6 | volumes: 7 | - ./test:/vpn:ro 8 | cap_add: 9 | - NET_ADMIN 10 | devices: 11 | - /dev/net/tun:/dev/net/tun 12 | dns: 10.0.0.1 13 | environment: 14 | - VPN_PASSWORD 15 | entrypoint: "sh -c 'echo $$VPN_PASSWORD | openvpn --cd /vpn/ --config lab.ovpn --askpass /dev/stdin'" 16 | -------------------------------------------------------------------------------- /test/lab.ovpn: -------------------------------------------------------------------------------- 1 | dev tun 2 | persist-tun 3 | persist-key 4 | cipher AES-256-CBC 5 | ncp-ciphers AES-256-GCM:AES-128-GCM 6 | auth SHA1 7 | tls-client 8 | client 9 | resolv-retry infinite 10 | remote 91.132.204.28 2000 tcp-client 11 | remote-cert-tls server 12 | 13 | pkcs12 lab.p12 14 | 15 | <tls-auth> 16 | # 17 | # 2048 bit OpenVPN static key 18 | # 19 | -----BEGIN OpenVPN Static key V1----- 20 | 6c9efab783fc2ee1a558bcedeaf92f8d 21 | 85322bc05432fbb00745fcd00bb48857 22 | 77cbf0c82462726a848657c56b62f6fd 23 | b9b1622c633188e848ce78c1b4476e9f 24 | 938338532c79784f36d80156e3b29bcf 25 | 493e64c393ee216b776c7a5d62c03aa8 26 | 5fc5fea73990612f07660988da133b61 27 | 34c847e67f65b8af407ae0b2761de402 28 | 49ede990747659a878acaaf8fa1a6201 29 | 1aa8ec5aeb01ccf50d1dc6e675dea291 30 | 8d4c199c1c126fee9c112ce16c736159 31 | 3234d5eaea167f5e60d01ad618fd33bb 32 | c262fb3d5227933d6149e45ab0246d58 33 | 5f5d66d835fbfc8e8d51e0462194d835 34 | 8f66f166ccef5616abba26dd38046a87 35 | 9476359e2dc7a5b4dc045e3fbe39d6e6 36 | -----END OpenVPN Static key V1----- 37 | </tls-auth> 38 | key-direction 1 39 | -------------------------------------------------------------------------------- /test/lab.p12: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jetbrains-infra/packer-builder-vsphere/bd2040307ce568a94887e41c644e20fa533f0fc7/test/lab.p12 -------------------------------------------------------------------------------- /test/test-key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEA2J9w3cbqMJSDTCUtFW3qRHhqgXbSOW32anqEWQYvW48WKXJm 3 | ZmuuSViC0tcAMCnX8pu5YGlAMCi5RBDtdoE9mZzUCfE4Q1Om42S2jKRrSSbhU9Ts 4 | 8jTRL0V81Tja64SEt5l1dDHS5sgNJy8C4nWaWob1HT+YloPEllj80ogwoQoL3ufp 5 | r5me/TOrA3ApHXewWm0feBkkkuN6NkL1Z9sILCstLrjD+RVEOvI/wrHZEaLpYJ4P 6 | LgS8LmTNKaFafmqwgcC4VcA4kVbhxw9X385v+mQLqpiOJa+vS51dT2qINEw+80Y+ 7 | HL7k7OIZTLg803wubI3rUZQ/2PX/STBq1zO9RwIDAQABAoIBAAmrDBGJ6Dfk2PtU 8 | CXAUaMlHipFeqUFQ7BeSgkeq5AA1IasV5QYbNjslzSj12ZdMtsuoMZzg9bFwj9w+ 9 | 2SpZ2FL70ebjsjwnBqLNguxCBlvMdXAVZ8Hjo5Z1hn3JvNOYJYhAPCLEeoI8WYHv 10 | MjTDRPFXZqc4iGnnVaXUMOyAkZMOV6sMQzvuJad4x7gvQGRhCgcdnFdGbVs+MZQc 11 | WPI6cO6imj27F6rJK3W6s5XcSjDbkpytf2wUuWYgck93Fdm3kYy3ER6B3P/MiM95 12 | qGRmg6OuEYbXAr4ytamjKUThl83SGvDS89N5SIjS5rgrEBgrOFBgMhjG/ibaxbrh 13 | c84oplECgYEA+vyI4VUYgce8voYmdDijlM/NwPbCpD3SGiyXIYcDN1i/CUdDhBYh 14 | z4982H6I1b2cg+veBWICro9Dp20CpfGtXT6Y3o1yNWkbKlosd+f2Us10fG1gkcyI 15 | TiZCYaJPrtdoTT0vMKbdUbkgn0FLNbW1TCh5FQ7K7RXhDonb9BbsTzkCgYEA3PMu 16 | bv/MgaET654GAItudazJmh4FfR905w59yVNJfe+7iG/f5zzv7vIpaERvBo245hcu 17 | IaO8QbW5OKYuCaNIjGOSd1uxN5ytcOHcf1bmjS+WRQdu/FR5v9BM0BY66NFjqKMb 18 | dZLXVZPnU3EOqCKmi9SI2VOVKrDL5XzMOHhL8H8CgYBFJh5wNomx993AgCVID/LB 19 | pR8C8vldVsrz+yUIT7JLJWA8pi2rzo0yKk4zN2lrufnNPsbEpOQoQ8BX+GiqX5Ns 20 | BTsI1d+JZ5Pcb0uhHX94ALL/NQNOKBPFtDTFwXpCqYZLAXhm5xJC2cZrGgommhGB 21 | EgWKD7FI8KY44zJ+ZXJlwQKBgGvw/eFKZI17tPCp3cLMW2VvyXnaatIK2SC8SqVd 22 | ZAz7XoG0Lg2ZDpqMgcAnlpn8CLWX43iZtjHf5qIPRXR96cZ0KqzXBcfmajE4lnE7 23 | chzNf7sve4AYgPY9fBk4kwUEroxHSvXwi/SJ8jwogoGPlA/CAC00ES6u+p2dj2OT 24 | GX5fAoGBAM6saTeyjAjLDE/vlPM9OButsoj5CJg7DklRgrRuRyygbyRBudafslnl 25 | 8e4+4mlXEBwKDnrDTtXFhX1Ur95/w/4GjyFXO/TB/Tmn+vaEBQTzgViKc2cJ/yay 26 | ttiF6oJh9EjCaFDTz5P11wX7DajRux/2tUcBXX/C3FcGhNEkVb2P 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test/test-key.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDYn3DdxuowlINMJS0VbepEeGqBdtI5bfZqeoRZBi9bjxYpcmZma65JWILS1wAwKdfym7lgaUAwKLlEEO12gT2ZnNQJ8ThDU6bjZLaMpGtJJuFT1OzyNNEvRXzVONrrhIS3mXV0MdLmyA0nLwLidZpahvUdP5iWg8SWWPzSiDChCgve5+mvmZ79M6sDcCkdd7BabR94GSSS43o2QvVn2wgsKy0uuMP5FUQ68j/CsdkRoulgng8uBLwuZM0poVp+arCBwLhVwDiRVuHHD1ffzm/6ZAuqmI4lr69LnV1Paog0TD7zRj4cvuTs4hlMuDzTfC5sjetRlD/Y9f9JMGrXM71H 2 | --------------------------------------------------------------------------------