├── .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 | [](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub)
2 | [](https://github.com/jetbrains-infra/packer-builder-vsphere/releases)
3 | [](https://github.com/jetbrains-infra/packer-builder-vsphere/releases)
4 | [](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
97 |
98 | Administrators
99 |
100 |
101 |
102 |
103 |
104 | true
105 | jetbrains
106 |
107 | jetbrains
108 | true
109 |
110 | 1
111 |
112 |
113 |
114 | 1
115 |
116 | powershell -ExecutionPolicy Bypass -File a:\setup.ps1
117 | true
118 |
119 |
120 |
121 |
122 |
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",
441 | "mount -t vfat /dev/fd0 /media/floppy",
442 | "setup-alpine -f /media/floppy/answerfile",
443 | "",
444 | "jetbrains",
445 | "jetbrains",
446 | "",
447 | "y",
448 | "",
449 | "reboot",
450 | "",
451 | "root",
452 | "jetbrains",
453 | "mount -t vfat /dev/fd0 /media/floppy",
454 | "/media/floppy/SETUP.SH",
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 | "": key.CodeReturnEnter,
50 | "": key.CodeEscape,
51 | "": key.CodeDeleteBackspace,
52 | "": key.CodeDeleteForward,
53 | "": key.CodeTab,
54 | "": key.CodeF1,
55 | "": key.CodeF2,
56 | "": key.CodeF3,
57 | "": key.CodeF4,
58 | "": key.CodeF5,
59 | "": key.CodeF6,
60 | "": key.CodeF7,
61 | "": key.CodeF8,
62 | "": key.CodeF9,
63 | "": key.CodeF10,
64 | "": key.CodeF11,
65 | "": key.CodeF12,
66 | "": key.CodeInsert,
67 | "": key.CodeHome,
68 | "": key.CodeEnd,
69 | "": key.CodePageUp,
70 | "": key.CodePageDown,
71 | "": key.CodeLeftArrow,
72 | "": key.CodeRightArrow,
73 | "": key.CodeUpArrow,
74 | "": 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, "") {
145 | log.Printf("Waiting 1 second")
146 | time.Sleep(1 * time.Second)
147 | message = message[len(""):]
148 | continue
149 | }
150 |
151 | if strings.HasPrefix(message, "") {
152 | log.Printf("Waiting 5 seconds")
153 | time.Sleep(5 * time.Second)
154 | message = message[len(""):]
155 | continue
156 | }
157 |
158 | if strings.HasPrefix(message, "") {
159 | log.Printf("Waiting 10 seconds")
160 | time.Sleep(10 * time.Second)
161 | message = message[len(""):]
162 | continue
163 | }
164 |
165 | if strings.HasPrefix(message, "") {
166 | keyAlt = true
167 | message = message[len(""):]
168 | continue
169 | }
170 |
171 | if strings.HasPrefix(message, "") {
172 | keyAlt = false
173 | message = message[len(""):]
174 | continue
175 | }
176 |
177 | if strings.HasPrefix(message, "") {
178 | keyCtrl = true
179 | message = message[len(""):]
180 | continue
181 | }
182 |
183 | if strings.HasPrefix(message, "") {
184 | keyCtrl = false
185 | message = message[len(""):]
186 | continue
187 | }
188 |
189 | if strings.HasPrefix(message, "") {
190 | keyShift = true
191 | message = message[len(""):]
192 | continue
193 | }
194 |
195 | if strings.HasPrefix(message, "") {
196 | keyShift = false
197 | message = message[len(""):]
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 |
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 |
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 |
--------------------------------------------------------------------------------