vendor qemu plugin

This commit is contained in:
sylviamoss 2021-04-19 16:32:04 +02:00
parent 642ed07476
commit 7a85b7328e
31 changed files with 3773 additions and 10 deletions

View File

@ -25,6 +25,7 @@ import (
googlecomputebuilder "github.com/hashicorp/packer-plugin-googlecompute/builder/googlecompute"
googlecomputeexportpostprocessor "github.com/hashicorp/packer-plugin-googlecompute/post-processor/googlecompute-export"
googlecomputeimportpostprocessor "github.com/hashicorp/packer-plugin-googlecompute/post-processor/googlecompute-import"
qemubuilder "github.com/hashicorp/packer-plugin-qemu/builder/qemu"
virtualboxisobuilder "github.com/hashicorp/packer-plugin-virtualbox/builder/virtualbox/iso"
virtualboxovfbuilder "github.com/hashicorp/packer-plugin-virtualbox/builder/virtualbox/ovf"
virtualboxvmbuilder "github.com/hashicorp/packer-plugin-virtualbox/builder/virtualbox/vm"
@ -53,6 +54,7 @@ var VendoredBuilders = map[string]packersdk.Builder{
"amazon-ebsvolume": new(amazonebsvolumebuilder.Builder),
"amazon-instance": new(amazoninstancebuilder.Builder),
"googlecompute": new(googlecomputebuilder.Builder),
"qemu": new(qemubuilder.Builder),
"vsphere-clone": new(vsphereclonebuilder.Builder),
"vsphere-iso": new(vsphereisobuilder.Builder),
"virtualbox-iso": new(virtualboxisobuilder.Builder),

3
go.mod
View File

@ -22,7 +22,6 @@ require (
github.com/cheggaaa/pb v1.0.27
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/digitalocean/go-qemu v0.0.0-20201211181942-d361e7b4965f
github.com/digitalocean/godo v1.11.1
github.com/dsnet/compress v0.0.1
github.com/exoscale/packer-plugin-exoscale v0.1.1
@ -50,6 +49,7 @@ require (
github.com/hashicorp/packer-plugin-ansible v0.0.2
github.com/hashicorp/packer-plugin-docker v0.0.7
github.com/hashicorp/packer-plugin-googlecompute v0.0.1
github.com/hashicorp/packer-plugin-qemu v0.0.0-20210419131938-9ec808d0c364
github.com/hashicorp/packer-plugin-sdk v0.2.0
github.com/hashicorp/packer-plugin-virtualbox v0.0.1
github.com/hashicorp/packer-plugin-vmware v0.0.1
@ -64,7 +64,6 @@ require (
github.com/mattn/go-tty v0.0.0-20191112051231-74040eebce08
github.com/mitchellh/cli v1.1.0
github.com/mitchellh/go-homedir v1.1.0
github.com/mitchellh/go-vnc v0.0.0-20150629162542-723ed9867aed
github.com/mitchellh/mapstructure v1.4.0
github.com/mitchellh/panicwrap v1.0.0
github.com/mitchellh/prefixedio v0.0.0-20151214002211-6e6954073784

7
go.sum
View File

@ -199,8 +199,9 @@ github.com/digitalocean/go-libvirt v0.0.0-20210108193637-3a8ae49ba8cd/go.mod h1:
github.com/digitalocean/go-libvirt v0.0.0-20210112203132-25518eb2c840 h1:F3RVNV8SLLNhkNFcbDTgD3wAPMcrMJW6xjjI0JXy9z8=
github.com/digitalocean/go-libvirt v0.0.0-20210112203132-25518eb2c840/go.mod h1:gtar3MgGsIO64GgphCHw1cbyxSI6qEuTIm9+izMmlfk=
github.com/digitalocean/go-qemu v0.0.0-20181112162955-dd7bb9c771b8/go.mod h1:/YnlngP1PARC0SKAZx6kaAEMOp8bNTQGqS+Ka3MctNI=
github.com/digitalocean/go-qemu v0.0.0-20201211181942-d361e7b4965f h1:BYkBJhHxUJJn27mhqfqWycWaEOWv9JQqLgQ2pOFJMqE=
github.com/digitalocean/go-qemu v0.0.0-20201211181942-d361e7b4965f/go.mod h1:y4Eq3ZfZQFWQwVyW0qvgo5seXUIq2C7BlHsdE+xtXL4=
github.com/digitalocean/go-qemu v0.0.0-20210326154740-ac9e0b687001 h1:WAg57gnaAWWjMAELcwHjc2xy0PoXQ5G+vn3+XS6s1jI=
github.com/digitalocean/go-qemu v0.0.0-20210326154740-ac9e0b687001/go.mod h1:IetBE52JfFxK46p2n2Rqm+p5Gx1gpu2hRHsrbnPOWZQ=
github.com/digitalocean/godo v1.11.1 h1:OsTh37YFKk+g6DnAOrkXJ9oDArTkRx5UTkBJ2EWAO38=
github.com/digitalocean/godo v1.11.1/go.mod h1:h6faOIcZ8lWIwNQ+DN7b3CgX4Kwby5T+nbpNqkUIozU=
github.com/dimchansky/utfbom v1.1.0 h1:FcM3g+nofKgUteL8dm/UpdRXNC9KmADgTpLKsu0TRo4=
@ -461,6 +462,8 @@ github.com/hashicorp/packer-plugin-docker v0.0.7 h1:hMTrH7vrkFIjphtbbtpuzffTzSjM
github.com/hashicorp/packer-plugin-docker v0.0.7/go.mod h1:IpeKlwOSy2kdgQcysqd3gCsoqjME9jtmpFoKxn7RRNI=
github.com/hashicorp/packer-plugin-googlecompute v0.0.1 h1:Shjio88MraB+ocj0VI5+M65r4UBKbYI4eCqLNyPXKEo=
github.com/hashicorp/packer-plugin-googlecompute v0.0.1/go.mod h1:MfV898IrEMpKH6wVnvOI5Tkhxm2snf3QxwVqV4k3bNI=
github.com/hashicorp/packer-plugin-qemu v0.0.0-20210419131938-9ec808d0c364 h1:9vTNg4xYdp+PzBFSi+G2Nfc8qw7X1ebrBFKt4wmvARk=
github.com/hashicorp/packer-plugin-qemu v0.0.0-20210419131938-9ec808d0c364/go.mod h1:8Q/LCjO7oplLcLe1KLdEt7rq94h42Di6Lab2DTLNwVg=
github.com/hashicorp/packer-plugin-sdk v0.0.6/go.mod h1:Nvh28f+Jmpp2rcaN79bULTouNkGNDRfHckhHKTAXtyU=
github.com/hashicorp/packer-plugin-sdk v0.0.7-0.20210111224258-fd30ebb797f0/go.mod h1:YdWTt5w6cYfaQG7IOi5iorL+3SXnz8hI0gJCi8Db/LI=
github.com/hashicorp/packer-plugin-sdk v0.0.7-0.20210120105339-f6fd68d2570a/go.mod h1:exN0C+Pe+3zu18l4nxueNjX5cfmslxUX/m/xk4IVmZQ=
@ -478,8 +481,6 @@ github.com/hashicorp/packer-plugin-sdk v0.2.0 h1:A4Dq7p4y1vscY4gMzp7GQaXyDJYYhP4
github.com/hashicorp/packer-plugin-sdk v0.2.0/go.mod h1:0DiOMEBldmB0HEhp0npFSSygC8bIvW43pphEgWkp2WU=
github.com/hashicorp/packer-plugin-virtualbox v0.0.1 h1:vTfy7a10RUVMdNnDLo0EQrCVbAG4rGWkaDTMC7MVBi4=
github.com/hashicorp/packer-plugin-virtualbox v0.0.1/go.mod h1:OOGNMK8Y8zjsYngesZH5kCbH0Fj8PKvhqPp8w1ejM3Y=
github.com/hashicorp/packer-plugin-vmware v0.0.0-20210416150724-592c0637562a h1:LpZ+8Y1vozsI4vuAjUpUOEzFl+jNSY4SL1kfGkVMmb8=
github.com/hashicorp/packer-plugin-vmware v0.0.0-20210416150724-592c0637562a/go.mod h1:NsiT4IOeDKf/aszQNX+/B1xHrfBR3RdUM3sSqANgNec=
github.com/hashicorp/packer-plugin-vmware v0.0.1 h1:jRQAdjHwg3zeCBb52KoZsuxugrHcQhjgQln72o9eGgM=
github.com/hashicorp/packer-plugin-vmware v0.0.1/go.mod h1:NsiT4IOeDKf/aszQNX+/B1xHrfBR3RdUM3sSqANgNec=
github.com/hashicorp/packer-plugin-vsphere v0.0.1 h1:4SUmRP+mGpBJHp6dLL4dmBCC+yDseTktb9YNLj11mVI=

View File

@ -18,8 +18,10 @@ import (
"bufio"
"context"
"encoding/json"
"fmt"
"io"
"net"
"os"
"sync"
"sync/atomic"
"time"
@ -34,6 +36,9 @@ type SocketMonitor struct {
// QEMU version reported by a connected monitor socket.
Version *Version
// QEMU QMP capabiltiies reported by a connected monitor socket.
Capabilities []string
// Underlying connection
c net.Conn
@ -119,6 +124,7 @@ func (mon *SocketMonitor) Connect() error {
return err
}
mon.Version = &ban.QMP.Version
mon.Capabilities = ban.QMP.Capabilities
// Issue capabilities handshake
cmd := Command{Execute: qmpCapabilities}
@ -194,13 +200,45 @@ func (mon *SocketMonitor) listen(r io.Reader, events chan<- Event, stream chan<-
// For a list of available QAPI commands, see:
// http://git.qemu.org/?p=qemu.git;a=blob;f=qapi-schema.json;hb=HEAD
func (mon *SocketMonitor) Run(command []byte) ([]byte, error) {
// Just call RunWithFile with no file
return mon.RunWithFile(command, nil)
}
// RunWithFile behaves like Run but allows for passing a file through out-of-band data.
func (mon *SocketMonitor) RunWithFile(command []byte, file *os.File) ([]byte, error) {
// Only allow a single command to be run at a time to ensure that responses
// to a command cannot be mixed with responses from another command
mon.mu.Lock()
defer mon.mu.Unlock()
if _, err := mon.c.Write(command); err != nil {
return nil, err
if file == nil {
// Just send a normal command through.
if _, err := mon.c.Write(command); err != nil {
return nil, err
}
} else {
unixConn, ok := mon.c.(*net.UnixConn)
if !ok {
return nil, fmt.Errorf("RunWithFile only works with unix monitor sockets")
}
oobSupported := false
for _, capability := range mon.Capabilities {
if capability == "oob" {
oobSupported = true
break
}
}
if !oobSupported {
return nil, fmt.Errorf("The QEMU server doesn't support oob (needed for RunWithFile)")
}
// Send the command along with the file descriptor.
oob := getUnixRights(file)
if _, _, err := unixConn.WriteMsgUnix(command, oob, nil); err != nil {
return nil, err
}
}
// Wait for a response or error to our command
@ -224,6 +262,7 @@ func (mon *SocketMonitor) Run(command []byte) ([]byte, error) {
// banner is a wrapper type around a Version.
type banner struct {
QMP struct {
Capabilities []string `json:"capabilities"`
Version Version `json:"version"`
} `json:"QMP"`
}

View File

@ -0,0 +1,27 @@
// Copyright 2016 The go-qemu Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// +build !windows
package qmp
import (
"os"
"golang.org/x/sys/unix"
)
func getUnixRights(file *os.File) []byte {
return unix.UnixRights(int(file.Fd()))
}

View File

@ -0,0 +1,25 @@
// Copyright 2016 The go-qemu Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// +build windows
package qmp
import (
"os"
)
func getUnixRights(file *os.File) []byte {
return nil
}

373
vendor/github.com/hashicorp/packer-plugin-qemu/LICENSE generated vendored Normal file
View File

@ -0,0 +1,373 @@
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.

View File

@ -0,0 +1,38 @@
package qemu
import (
"fmt"
"os"
)
// Artifact is the result of running the Qemu builder, namely a set
// of files associated with the resulting machine.
type Artifact struct {
dir string
f []string
state map[string]interface{}
}
func (*Artifact) BuilderId() string {
return BuilderId
}
func (a *Artifact) Files() []string {
return a.f
}
func (*Artifact) Id() string {
return "VM"
}
func (a *Artifact) String() string {
return fmt.Sprintf("VM files in directory: %s", a.dir)
}
func (a *Artifact) State(name string) interface{} {
return a.state[name]
}
func (a *Artifact) Destroy() error {
return os.RemoveAll(a.dir)
}

View File

@ -0,0 +1,229 @@
package qemu
import (
"context"
"errors"
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"github.com/hashicorp/hcl/v2/hcldec"
"github.com/hashicorp/packer-plugin-sdk/communicator"
"github.com/hashicorp/packer-plugin-sdk/multistep"
"github.com/hashicorp/packer-plugin-sdk/multistep/commonsteps"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
)
const BuilderId = "transcend.qemu"
type Builder struct {
config Config
runner multistep.Runner
}
func (b *Builder) ConfigSpec() hcldec.ObjectSpec { return b.config.FlatMapstructure().HCL2Spec() }
func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) {
warnings, errs := b.config.Prepare(raws...)
if errs != nil {
return nil, warnings, errs
}
return nil, warnings, nil
}
func (b *Builder) Run(ctx context.Context, ui packersdk.Ui, hook packersdk.Hook) (packersdk.Artifact, error) {
// Create the driver that we'll use to communicate with Qemu
driver, err := b.newDriver(b.config.QemuBinary)
if err != nil {
return nil, fmt.Errorf("Failed creating Qemu driver: %s", err)
}
steps := []multistep.Step{}
if !b.config.ISOSkipCache {
steps = append(steps, &commonsteps.StepDownload{
Checksum: b.config.ISOChecksum,
Description: "ISO",
Extension: b.config.TargetExtension,
ResultKey: "iso_path",
TargetPath: b.config.TargetPath,
Url: b.config.ISOUrls,
})
} else {
steps = append(steps, &stepSetISO{
ResultKey: "iso_path",
Url: b.config.ISOUrls,
})
}
steps = append(steps, new(stepPrepareOutputDir),
&commonsteps.StepCreateFloppy{
Files: b.config.FloppyConfig.FloppyFiles,
Directories: b.config.FloppyConfig.FloppyDirectories,
Label: b.config.FloppyConfig.FloppyLabel,
},
&commonsteps.StepCreateCD{
Files: b.config.CDConfig.CDFiles,
Label: b.config.CDConfig.CDLabel,
},
&stepCreateDisk{
AdditionalDiskSize: b.config.AdditionalDiskSize,
DiskImage: b.config.DiskImage,
DiskSize: b.config.DiskSize,
Format: b.config.Format,
OutputDir: b.config.OutputDir,
UseBackingFile: b.config.UseBackingFile,
VMName: b.config.VMName,
QemuImgArgs: b.config.QemuImgArgs,
},
&stepCopyDisk{
DiskImage: b.config.DiskImage,
Format: b.config.Format,
OutputDir: b.config.OutputDir,
UseBackingFile: b.config.UseBackingFile,
VMName: b.config.VMName,
},
&stepResizeDisk{
DiskCompression: b.config.DiskCompression,
DiskImage: b.config.DiskImage,
Format: b.config.Format,
OutputDir: b.config.OutputDir,
SkipResizeDisk: b.config.SkipResizeDisk,
VMName: b.config.VMName,
DiskSize: b.config.DiskSize,
QemuImgArgs: b.config.QemuImgArgs,
},
new(stepHTTPIPDiscover),
commonsteps.HTTPServerFromHTTPConfig(&b.config.HTTPConfig),
&stepPortForward{
CommunicatorType: b.config.CommConfig.Comm.Type,
NetBridge: b.config.NetBridge,
},
new(stepConfigureVNC),
&stepRun{
DiskImage: b.config.DiskImage,
},
&stepConfigureQMP{
QMPSocketPath: b.config.QMPSocketPath,
},
&stepTypeBootCommand{},
&stepWaitGuestAddress{
CommunicatorType: b.config.CommConfig.Comm.Type,
NetBridge: b.config.NetBridge,
timeout: b.config.CommConfig.Comm.SSHTimeout,
},
&communicator.StepConnect{
Config: &b.config.CommConfig.Comm,
Host: commHost(b.config.CommConfig.Comm.Host()),
SSHConfig: b.config.CommConfig.Comm.SSHConfigFunc(),
SSHPort: commPort,
WinRMPort: commPort,
},
new(commonsteps.StepProvision),
&commonsteps.StepCleanupTempKeys{
Comm: &b.config.CommConfig.Comm,
},
&stepShutdown{
ShutdownTimeout: b.config.ShutdownTimeout,
ShutdownCommand: b.config.ShutdownCommand,
Comm: &b.config.CommConfig.Comm,
},
&stepConvertDisk{
DiskCompression: b.config.DiskCompression,
Format: b.config.Format,
OutputDir: b.config.OutputDir,
SkipCompaction: b.config.SkipCompaction,
VMName: b.config.VMName,
QemuImgArgs: b.config.QemuImgArgs,
},
)
// Setup the state bag
state := new(multistep.BasicStateBag)
state.Put("config", &b.config)
state.Put("debug", b.config.PackerDebug)
state.Put("driver", driver)
state.Put("hook", hook)
state.Put("ui", ui)
// Run
b.runner = commonsteps.NewRunnerWithPauseFn(steps, b.config.PackerConfig, ui, state)
b.runner.Run(ctx, state)
// If there was an error, return that
if rawErr, ok := state.GetOk("error"); ok {
return nil, rawErr.(error)
}
// If we were interrupted or cancelled, then just exit.
if _, ok := state.GetOk(multistep.StateCancelled); ok {
return nil, errors.New("Build was cancelled.")
}
if _, ok := state.GetOk(multistep.StateHalted); ok {
return nil, errors.New("Build was halted.")
}
// Compile the artifact list
files := make([]string, 0, 5)
visit := func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() {
files = append(files, path)
}
return nil
}
if err := filepath.Walk(b.config.OutputDir, visit); err != nil {
return nil, err
}
artifact := &Artifact{
dir: b.config.OutputDir,
f: files,
state: make(map[string]interface{}),
}
artifact.state["generated_data"] = state.Get("generated_data")
artifact.state["diskName"] = b.config.VMName
// placed in state in step_create_disk.go
diskpaths, ok := state.Get("qemu_disk_paths").([]string)
if ok {
artifact.state["diskPaths"] = diskpaths
}
artifact.state["diskType"] = b.config.Format
artifact.state["diskSize"] = b.config.DiskSize
artifact.state["domainType"] = b.config.Accelerator
return artifact, nil
}
func (b *Builder) newDriver(qemuBinary string) (Driver, error) {
qemuPath, err := exec.LookPath(qemuBinary)
if err != nil {
return nil, err
}
qemuImgPath, err := exec.LookPath("qemu-img")
if err != nil {
return nil, err
}
log.Printf("Qemu path: %s, Qemu Image page: %s", qemuPath, qemuImgPath)
driver := &QemuDriver{
QemuPath: qemuPath,
QemuImgPath: qemuImgPath,
}
if err := driver.Verify(); err != nil {
return nil, err
}
return driver, nil
}

View File

@ -0,0 +1,73 @@
//go:generate packer-sdc struct-markdown
package qemu
import (
"errors"
"github.com/hashicorp/packer-plugin-sdk/communicator"
"github.com/hashicorp/packer-plugin-sdk/template/interpolate"
)
type CommConfig struct {
Comm communicator.Config `mapstructure:",squash"`
// The minimum port to use for the Communicator port on the host machine which is forwarded
// to the SSH or WinRM port on the guest machine. By default this is 2222.
HostPortMin int `mapstructure:"host_port_min" required:"false"`
// The maximum port to use for the Communicator port on the host machine which is forwarded
// to the SSH or WinRM port on the guest machine. Because Packer often runs in parallel,
// Packer will choose a randomly available port in this range to use as the
// host port. By default this is 4444.
HostPortMax int `mapstructure:"host_port_max" required:"false"`
// Defaults to false. When enabled, Packer
// does not setup forwarded port mapping for communicator (SSH or WinRM) requests and uses ssh_port or winrm_port
// on the host to communicate to the virtual machine.
SkipNatMapping bool `mapstructure:"skip_nat_mapping" required:"false"`
// These are deprecated, but we keep them around for backwards compatibility
// TODO: remove later
SSHHostPortMin int `mapstructure:"ssh_host_port_min" required:"false"`
// TODO: remove later
SSHHostPortMax int `mapstructure:"ssh_host_port_max"`
}
func (c *CommConfig) Prepare(ctx *interpolate.Context) (warnings []string, errs []error) {
// Backwards compatibility
if c.SSHHostPortMin != 0 {
warnings = append(warnings, "ssh_host_port_min is deprecated and is being replaced by host_port_min. "+
"Please, update your template to use host_port_min. In future versions of Packer, inclusion of ssh_host_port_min will error your builds.")
c.HostPortMin = c.SSHHostPortMin
}
// Backwards compatibility
if c.SSHHostPortMax != 0 {
warnings = append(warnings, "ssh_host_port_max is deprecated and is being replaced by host_port_max. "+
"Please, update your template to use host_port_max. In future versions of Packer, inclusion of ssh_host_port_max will error your builds.")
c.HostPortMax = c.SSHHostPortMax
}
if c.Comm.SSHHost == "" && c.SkipNatMapping {
c.Comm.SSHHost = "127.0.0.1"
c.Comm.WinRMHost = "127.0.0.1"
}
if c.HostPortMin == 0 {
c.HostPortMin = 2222
}
if c.HostPortMax == 0 {
c.HostPortMax = 4444
}
errs = c.Comm.Prepare(ctx)
if c.HostPortMin > c.HostPortMax {
errs = append(errs,
errors.New("host_port_min must be less than host_port_max"))
}
if c.HostPortMin < 0 {
errs = append(errs, errors.New("host_port_min must be positive"))
}
return
}

View File

@ -0,0 +1,642 @@
//go:generate packer-sdc struct-markdown
//go:generate packer-sdc mapstructure-to-hcl2 -type Config,QemuImgArgs
package qemu
import (
"errors"
"fmt"
"log"
"os"
"path/filepath"
"regexp"
"runtime"
"strings"
"github.com/hashicorp/packer-plugin-sdk/bootcommand"
"github.com/hashicorp/packer-plugin-sdk/common"
"github.com/hashicorp/packer-plugin-sdk/multistep/commonsteps"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
"github.com/hashicorp/packer-plugin-sdk/shutdowncommand"
"github.com/hashicorp/packer-plugin-sdk/template/config"
"github.com/hashicorp/packer-plugin-sdk/template/interpolate"
)
var accels = map[string]struct{}{
"none": {},
"kvm": {},
"tcg": {},
"xen": {},
"hax": {},
"hvf": {},
"whpx": {},
}
var diskInterface = map[string]bool{
"ide": true,
"scsi": true,
"virtio": true,
"virtio-scsi": true,
}
var diskCache = map[string]bool{
"writethrough": true,
"writeback": true,
"none": true,
"unsafe": true,
"directsync": true,
}
var diskDiscard = map[string]bool{
"unmap": true,
"ignore": true,
}
var diskDZeroes = map[string]bool{
"unmap": true,
"on": true,
"off": true,
}
type QemuImgArgs struct {
Convert []string `mapstructure:"convert" required:"false"`
Create []string `mapstructure:"create" required:"false"`
Resize []string `mapstructure:"resize" required:"false"`
}
type Config struct {
common.PackerConfig `mapstructure:",squash"`
commonsteps.HTTPConfig `mapstructure:",squash"`
commonsteps.ISOConfig `mapstructure:",squash"`
bootcommand.VNCConfig `mapstructure:",squash"`
shutdowncommand.ShutdownConfig `mapstructure:",squash"`
CommConfig CommConfig `mapstructure:",squash"`
commonsteps.FloppyConfig `mapstructure:",squash"`
commonsteps.CDConfig `mapstructure:",squash"`
// Use iso from provided url. Qemu must support
// curl block device. This defaults to `false`.
ISOSkipCache bool `mapstructure:"iso_skip_cache" required:"false"`
// The accelerator type to use when running the VM.
// This may be `none`, `kvm`, `tcg`, `hax`, `hvf`, `whpx`, or `xen`. The appropriate
// software must have already been installed on your build machine to use the
// accelerator you specified. When no accelerator is specified, Packer will try
// to use `kvm` if it is available but will default to `tcg` otherwise.
//
// ~> The `hax` accelerator has issues attaching CDROM ISOs. This is an
// upstream issue which can be tracked
// [here](https://github.com/intel/haxm/issues/20).
//
// ~> The `hvf` and `whpx` accelerator are new and experimental as of
// [QEMU 2.12.0](https://wiki.qemu.org/ChangeLog/2.12#Host_support).
// You may encounter issues unrelated to Packer when using these. You may need to
// add [ "-global", "virtio-pci.disable-modern=on" ] to `qemuargs` depending on the
// guest operating system.
//
// ~> For `whpx`, note that [Stefan Weil's QEMU for Windows distribution](https://qemu.weilnetz.de/w64/)
// does not include WHPX support and users may need to compile or source a
// build of QEMU for Windows themselves with WHPX support.
Accelerator string `mapstructure:"accelerator" required:"false"`
// Additional disks to create. Uses `vm_name` as the disk name template and
// appends `-#` where `#` is the position in the array. `#` starts at 1 since 0
// is the default disk. Each string represents the disk image size in bytes.
// Optional suffixes 'k' or 'K' (kilobyte, 1024), 'M' (megabyte, 1024k), 'G'
// (gigabyte, 1024M), 'T' (terabyte, 1024G), 'P' (petabyte, 1024T) and 'E'
// (exabyte, 1024P) are supported. 'b' is ignored. Per qemu-img documentation.
// Each additional disk uses the same disk parameters as the default disk.
// Unset by default.
AdditionalDiskSize []string `mapstructure:"disk_additional_size" required:"false"`
// The number of cpus to use when building the VM.
// The default is `1` CPU.
CpuCount int `mapstructure:"cpus" required:"false"`
// The firmware file to be used by QEMU, which is to be set by the -bios
// option of QEMU. Particularly, this option can be set to use EFI instead
// of BIOS, by using "OVMF.fd" from OpenFirmware.
// If unset, no -bios option is passed to QEMU, using the default of QEMU.
// Also see the QEMU documentation.
Firmware string `mapstructure:"firmware" required:"false"`
// The interface to use for the disk. Allowed values include any of `ide`,
// `scsi`, `virtio` or `virtio-scsi`^\*. Note also that any boot commands
// or kickstart type scripts must have proper adjustments for resulting
// device names. The Qemu builder uses `virtio` by default.
//
// ^\* Please be aware that use of the `scsi` disk interface has been
// disabled by Red Hat due to a bug described
// [here](https://bugzilla.redhat.com/show_bug.cgi?id=1019220). If you are
// running Qemu on RHEL or a RHEL variant such as CentOS, you *must* choose
// one of the other listed interfaces. Using the `scsi` interface under
// these circumstances will cause the build to fail.
DiskInterface string `mapstructure:"disk_interface" required:"false"`
// The size in bytes of the hard disk of the VM. Suffix with the first
// letter of common byte types. Use "k" or "K" for kilobytes, "M" for
// megabytes, G for gigabytes, and T for terabytes. If no value is provided
// for disk_size, Packer uses a default of `40960M` (40 GB). If a disk_size
// number is provided with no units, Packer will default to Megabytes.
DiskSize string `mapstructure:"disk_size" required:"false"`
// Packer resizes the QCOW2 image using
// qemu-img resize. Set this option to true to disable resizing.
// Defaults to false.
SkipResizeDisk bool `mapstructure:"skip_resize_disk" required:"false"`
// The cache mode to use for disk. Allowed values include any of
// `writethrough`, `writeback`, `none`, `unsafe` or `directsync`. By
// default, this is set to `writeback`.
DiskCache string `mapstructure:"disk_cache" required:"false"`
// The discard mode to use for disk. Allowed values
// include any of unmap or ignore. By default, this is set to ignore.
DiskDiscard string `mapstructure:"disk_discard" required:"false"`
// The detect-zeroes mode to use for disk.
// Allowed values include any of unmap, on or off. Defaults to off.
// When the value is "off" we don't set the flag in the qemu command, so that
// Packer still works with old versions of QEMU that don't have this option.
DetectZeroes string `mapstructure:"disk_detect_zeroes" required:"false"`
// Packer compacts the QCOW2 image using
// qemu-img convert. Set this option to true to disable compacting.
// Defaults to false.
SkipCompaction bool `mapstructure:"skip_compaction" required:"false"`
// Apply compression to the QCOW2 disk file
// using qemu-img convert. Defaults to false.
DiskCompression bool `mapstructure:"disk_compression" required:"false"`
// Either `qcow2` or `raw`, this specifies the output format of the virtual
// machine image. This defaults to `qcow2`. Due to a long-standing bug with
// `qemu-img convert` on OSX, sometimes the qemu-img convert call will
// create a corrupted image. If this is an issue for you, make sure that the
// the output format matches the input file's format, and Packer will
// perform a simple copy operation instead. See
// https://bugs.launchpad.net/qemu/+bug/1776920 for more details.
Format string `mapstructure:"format" required:"false"`
// Packer defaults to building QEMU virtual machines by
// launching a GUI that shows the console of the machine being built. When this
// value is set to `true`, the machine will start without a console.
//
// You can still see the console if you make a note of the VNC display
// number chosen, and then connect using `vncviewer -Shared <host>:<display>`
Headless bool `mapstructure:"headless" required:"false"`
// Packer defaults to building from an ISO file, this parameter controls
// whether the ISO URL supplied is actually a bootable QEMU image. When
// this value is set to `true`, the machine will either clone the source or
// use it as a backing file (if `use_backing_file` is `true`); then, it
// will resize the image according to `disk_size` and boot it.
DiskImage bool `mapstructure:"disk_image" required:"false"`
// Only applicable when disk_image is true
// and format is qcow2, set this option to true to create a new QCOW2
// file that uses the file located at iso_url as a backing file. The new file
// will only contain blocks that have changed compared to the backing file, so
// enabling this option can significantly reduce disk usage. If true, Packer
// will force the `skip_compaction` also to be true as well to skip disk
// conversion which would render the backing file feature useless.
UseBackingFile bool `mapstructure:"use_backing_file" required:"false"`
// The type of machine emulation to use. Run your qemu binary with the
// flags `-machine help` to list available types for your system. This
// defaults to `pc`.
MachineType string `mapstructure:"machine_type" required:"false"`
// The amount of memory to use when building the VM
// in megabytes. This defaults to 512 megabytes.
MemorySize int `mapstructure:"memory" required:"false"`
// The driver to use for the network interface. Allowed values `ne2k_pci`,
// `i82551`, `i82557b`, `i82559er`, `rtl8139`, `e1000`, `pcnet`, `virtio`,
// `virtio-net`, `virtio-net-pci`, `usb-net`, `i82559a`, `i82559b`,
// `i82559c`, `i82550`, `i82562`, `i82557a`, `i82557c`, `i82801`,
// `vmxnet3`, `i82558a` or `i82558b`. The Qemu builder uses `virtio-net` by
// default.
NetDevice string `mapstructure:"net_device" required:"false"`
// Connects the network to this bridge instead of using the user mode
// networking.
//
// **NB** This bridge must already exist. You can use the `virbr0` bridge
// as created by vagrant-libvirt.
//
// **NB** This will automatically enable the QMP socket (see QMPEnable).
//
// **NB** This only works in Linux based OSes.
NetBridge string `mapstructure:"net_bridge" required:"false"`
// This is the path to the directory where the
// resulting virtual machine will be created. This may be relative or absolute.
// If relative, the path is relative to the working directory when packer
// is executed. This directory must not exist or be empty prior to running
// the builder. By default this is output-BUILDNAME where "BUILDNAME" is the
// name of the build.
OutputDir string `mapstructure:"output_directory" required:"false"`
// Allows complete control over the qemu command line (though not qemu-img).
// Each array of strings makes up a command line switch
// that overrides matching default switch/value pairs. Any value specified
// as an empty string is ignored. All values after the switch are
// concatenated with no separator.
//
// ~> **Warning:** The qemu command line allows extreme flexibility, so
// beware of conflicting arguments causing failures of your run.
// For instance adding a "--drive" or "--device" override will mean that
// none of the default configuration Packer sets will be used. To see the
// defaults that Packer sets, look in your packer.log
// file (set PACKER_LOG=1 to get verbose logging) and search for the
// qemu-system-x86 command. The arguments are all printed for review, and
// you can use those arguments along with the template engines allowed
// by qemu-args to set up a working configuration that includes both the
// Packer defaults and your extra arguments.
//
// Another pitfall could be setting arguments like --no-acpi, which could
// break the ability to send power signal type commands
// (e.g., shutdown -P now) to the virtual machine, thus preventing proper
// shutdown.
//
// The following shows a sample usage:
//
// In JSON:
// ```json
// "qemuargs": [
// [ "-m", "1024M" ],
// [ "--no-acpi", "" ],
// [
// "-netdev",
// "user,id=mynet0,",
// "hostfwd=hostip:hostport-guestip:guestport",
// ""
// ],
// [ "-device", "virtio-net,netdev=mynet0" ]
// ]
// ```
//
// In HCL2:
// ```hcl
// qemuargs = [
// [ "-m", "1024M" ],
// [ "--no-acpi", "" ],
// [
// "-netdev",
// "user,id=mynet0,",
// "hostfwd=hostip:hostport-guestip:guestport",
// ""
// ],
// [ "-device", "virtio-net,netdev=mynet0" ]
// ]
// ```
//
// would produce the following (not including other defaults supplied by
// the builder and not otherwise conflicting with the qemuargs):
//
// ```text
// qemu-system-x86 -m 1024m --no-acpi -netdev
// user,id=mynet0,hostfwd=hostip:hostport-guestip:guestport -device
// virtio-net,netdev=mynet0"
// ```
//
// ~> **Windows Users:** [QEMU for Windows](https://qemu.weilnetz.de/)
// builds are available though an environmental variable does need to be
// set for QEMU for Windows to redirect stdout to the console instead of
// stdout.txt.
//
// The following shows the environment variable that needs to be set for
// Windows QEMU support:
//
// ```text
// setx SDL_STDIO_REDIRECT=0
// ```
//
// You can also use the `SSHHostPort` template variable to produce a packer
// template that can be invoked by `make` in parallel:
//
// In JSON:
// ```json
// "qemuargs": [
// [ "-netdev", "user,hostfwd=tcp::{{ .SSHHostPort }}-:22,id=forward"],
// [ "-device", "virtio-net,netdev=forward,id=net0"]
// ]
// ```
//
// In HCL2:
// ```hcl
// qemuargs = [
// [ "-netdev", "user,hostfwd=tcp::{{ .SSHHostPort }}-:22,id=forward"],
// [ "-device", "virtio-net,netdev=forward,id=net0"]
// ]
//
// `make -j 3 my-awesome-packer-templates` spawns 3 packer processes, each
// of which will bind to their own SSH port as determined by each process.
// This will also work with WinRM, just change the port forward in
// `qemuargs` to map to WinRM's default port of `5985` or whatever value
// you have the service set to listen on.
//
// This is a template engine and allows access to the following variables:
// `{{ .HTTPIP }}`, `{{ .HTTPPort }}`, `{{ .HTTPDir }}`,
// `{{ .OutputDir }}`, `{{ .Name }}`, and `{{ .SSHHostPort }}`
QemuArgs [][]string `mapstructure:"qemuargs" required:"false"`
// A map of custom arguments to pass to qemu-img commands, where the key
// is the subcommand, and the values are lists of strings for each flag.
// Example:
//
// In JSON:
// ```json
// {
// "qemu_img_args": {
// "convert": ["-o", "preallocation=full"],
// "resize": ["-foo", "bar"]
// }
// ```
// Please note
// that unlike qemuargs, these commands are not split into switch-value
// sub-arrays, because the basic elements in qemu-img calls are unlikely
// to need an actual override.
// The arguments will be constructed as follows:
// - Convert:
// Default is `qemu-img convert -O $format $sourcepath $targetpath`. Adding
// arguments ["-foo", "bar"] to qemu_img_args.convert will change this to
// `qemu-img convert -foo bar -O $format $sourcepath $targetpath`
// - Create:
// Default is `create -f $format $targetpath $size`. Adding arguments
// ["-foo", "bar"] to qemu_img_args.create will change this to
// "create -f qcow2 -foo bar target.qcow2 1234M"
// - Resize:
// Default is `qemu-img resize -f $format $sourcepath $size`. Adding
// arguments ["-foo", "bar"] to qemu_img_args.resize will change this to
// `qemu-img resize -f $format -foo bar $sourcepath $size`
QemuImgArgs QemuImgArgs `mapstructure:"qemu_img_args" required:"false"`
// The name of the Qemu binary to look for. This
// defaults to qemu-system-x86_64, but may need to be changed for
// some platforms. For example qemu-kvm, or qemu-system-i386 may be a
// better choice for some systems.
QemuBinary string `mapstructure:"qemu_binary" required:"false"`
// Enable QMP socket. Location is specified by `qmp_socket_path`. Defaults
// to false.
QMPEnable bool `mapstructure:"qmp_enable" required:"false"`
// QMP Socket Path when `qmp_enable` is true. Defaults to
// `output_directory`/`vm_name`.monitor.
QMPSocketPath string `mapstructure:"qmp_socket_path" required:"false"`
// If true, do not pass a -display option
// to qemu, allowing it to choose the default. This may be needed when running
// under macOS, and getting errors about sdl not being available.
UseDefaultDisplay bool `mapstructure:"use_default_display" required:"false"`
// What QEMU -display option to use. Defaults to gtk, use none to not pass the
// -display option allowing QEMU to choose the default. This may be needed when
// running under macOS, and getting errors about sdl not being available.
Display string `mapstructure:"display" required:"false"`
// The IP address that should be
// binded to for VNC. By default packer will use 127.0.0.1 for this. If you
// wish to bind to all interfaces use 0.0.0.0.
VNCBindAddress string `mapstructure:"vnc_bind_address" required:"false"`
// Whether or not to set a password on the VNC server. This option
// automatically enables the QMP socket. See `qmp_socket_path`. Defaults to
// `false`.
VNCUsePassword bool `mapstructure:"vnc_use_password" required:"false"`
// The minimum and maximum port
// to use for VNC access to the virtual machine. The builder uses VNC to type
// the initial boot_command. Because Packer generally runs in parallel,
// Packer uses a randomly chosen port in this range that appears available. By
// default this is 5900 to 6000. The minimum and maximum ports are inclusive.
// The minimum port cannot be set below 5900 due to a quirk in how QEMU parses
// vnc display address.
VNCPortMin int `mapstructure:"vnc_port_min" required:"false"`
VNCPortMax int `mapstructure:"vnc_port_max"`
// This is the name of the image (QCOW2 or IMG) file for
// the new virtual machine. By default this is packer-BUILDNAME, where
// "BUILDNAME" is the name of the build. Currently, no file extension will be
// used unless it is specified in this option.
VMName string `mapstructure:"vm_name" required:"false"`
// The interface to use for the CDROM device which contains the ISO image.
// Allowed values include any of `ide`, `scsi`, `virtio` or
// `virtio-scsi`. The Qemu builder uses `virtio` by default.
// Some ARM64 images require `virtio-scsi`.
CDROMInterface string `mapstructure:"cdrom_interface" required:"false"`
// TODO(mitchellh): deprecate
RunOnce bool `mapstructure:"run_once"`
ctx interpolate.Context
}
func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
err := config.Decode(c, &config.DecodeOpts{
PluginType: BuilderId,
Interpolate: true,
InterpolateContext: &c.ctx,
InterpolateFilter: &interpolate.RenderFilter{
Exclude: []string{
"boot_command",
"qemuargs",
},
},
}, raws...)
if err != nil {
return nil, err
}
// Accumulate any errors and warnings
var errs *packersdk.MultiError
warnings := make([]string, 0)
errs = packersdk.MultiErrorAppend(errs, c.ShutdownConfig.Prepare(&c.ctx)...)
if c.DiskSize == "" || c.DiskSize == "0" {
c.DiskSize = "40960M"
} else {
// Make sure supplied disk size is valid
// (digits, plus an optional valid unit character). e.g. 5000, 40G, 1t
re := regexp.MustCompile(`^[\d]+(b|k|m|g|t){0,1}$`)
matched := re.MatchString(strings.ToLower(c.DiskSize))
if !matched {
errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("Invalid disk size."))
} else {
// Okay, it's valid -- if it doesn't alreay have a suffix, then
// append "M" as the default unit.
re = regexp.MustCompile(`^[\d]+$`)
matched = re.MatchString(strings.ToLower(c.DiskSize))
if matched {
// Needs M added.
c.DiskSize = fmt.Sprintf("%sM", c.DiskSize)
}
}
}
if c.DiskCache == "" {
c.DiskCache = "writeback"
}
if c.DiskDiscard == "" {
c.DiskDiscard = "ignore"
}
if c.DetectZeroes == "" {
c.DetectZeroes = "off"
}
if c.Accelerator == "" {
if runtime.GOOS == "windows" {
c.Accelerator = "tcg"
} else {
// /dev/kvm is a kernel module that may be loaded if kvm is
// installed and the host supports VT-x extensions. To make sure
// this will actually work we need to os.Open() it. If os.Open fails
// the kernel module was not installed or loaded correctly.
if fp, err := os.Open("/dev/kvm"); err != nil {
c.Accelerator = "tcg"
} else {
fp.Close()
c.Accelerator = "kvm"
}
}
log.Printf("use detected accelerator: %s", c.Accelerator)
} else {
log.Printf("use specified accelerator: %s", c.Accelerator)
}
if c.MachineType == "" {
c.MachineType = "pc"
}
if c.OutputDir == "" {
c.OutputDir = fmt.Sprintf("output-%s", c.PackerBuildName)
}
if c.QemuBinary == "" {
c.QemuBinary = "qemu-system-x86_64"
}
if c.MemorySize < 10 {
log.Printf("MemorySize %d is too small, using default: 512", c.MemorySize)
c.MemorySize = 512
}
if c.CpuCount < 1 {
log.Printf("CpuCount %d too small, using default: 1", c.CpuCount)
c.CpuCount = 1
}
if c.VNCBindAddress == "" {
c.VNCBindAddress = "127.0.0.1"
}
if c.VNCPortMin == 0 {
c.VNCPortMin = 5900
}
if c.VNCPortMax == 0 {
c.VNCPortMax = 6000
}
if c.VMName == "" {
c.VMName = fmt.Sprintf("packer-%s", c.PackerBuildName)
}
if c.Format == "" {
c.Format = "qcow2"
}
errs = packersdk.MultiErrorAppend(errs, c.FloppyConfig.Prepare(&c.ctx)...)
errs = packersdk.MultiErrorAppend(errs, c.CDConfig.Prepare(&c.ctx)...)
errs = packersdk.MultiErrorAppend(errs, c.VNCConfig.Prepare(&c.ctx)...)
if c.NetDevice == "" {
c.NetDevice = "virtio-net"
}
if c.DiskInterface == "" {
c.DiskInterface = "virtio"
}
if c.ISOSkipCache {
c.ISOChecksum = "none"
}
isoWarnings, isoErrs := c.ISOConfig.Prepare(&c.ctx)
warnings = append(warnings, isoWarnings...)
errs = packersdk.MultiErrorAppend(errs, isoErrs...)
errs = packersdk.MultiErrorAppend(errs, c.HTTPConfig.Prepare(&c.ctx)...)
commConfigWarnings, es := c.CommConfig.Prepare(&c.ctx)
if len(es) > 0 {
errs = packersdk.MultiErrorAppend(errs, es...)
}
warnings = append(warnings, commConfigWarnings...)
if !(c.Format == "qcow2" || c.Format == "raw") {
errs = packersdk.MultiErrorAppend(
errs, errors.New("invalid format, only 'qcow2' or 'raw' are allowed"))
}
if c.Format != "qcow2" {
c.SkipCompaction = true
c.DiskCompression = false
}
if c.UseBackingFile {
c.SkipCompaction = true
if !(c.DiskImage && c.Format == "qcow2") {
errs = packersdk.MultiErrorAppend(
errs, errors.New("use_backing_file can only be enabled for QCOW2 images and when disk_image is true"))
}
}
if c.SkipResizeDisk && !(c.DiskImage) {
errs = packersdk.MultiErrorAppend(
errs, errors.New("skip_resize_disk can only be used when disk_image is true"))
}
if _, ok := accels[c.Accelerator]; !ok {
errs = packersdk.MultiErrorAppend(
errs, errors.New("invalid accelerator, only 'kvm', 'tcg', 'xen', 'hax', 'hvf', 'whpx', or 'none' are allowed"))
}
if _, ok := diskInterface[c.DiskInterface]; !ok {
errs = packersdk.MultiErrorAppend(
errs, errors.New("unrecognized disk interface type"))
}
if _, ok := diskCache[c.DiskCache]; !ok {
errs = packersdk.MultiErrorAppend(
errs, errors.New("unrecognized disk cache type"))
}
if _, ok := diskDiscard[c.DiskDiscard]; !ok {
errs = packersdk.MultiErrorAppend(
errs, errors.New("unrecognized disk discard type"))
}
if _, ok := diskDZeroes[c.DetectZeroes]; !ok {
errs = packersdk.MultiErrorAppend(
errs, errors.New("unrecognized disk detect zeroes setting"))
}
if !c.PackerForce {
if _, err := os.Stat(c.OutputDir); err == nil {
errs = packersdk.MultiErrorAppend(
errs,
fmt.Errorf("Output directory '%s' already exists. It must not exist.", c.OutputDir))
}
}
if c.VNCPortMin < 5900 {
errs = packersdk.MultiErrorAppend(
errs, fmt.Errorf("vnc_port_min cannot be below 5900"))
}
if c.VNCPortMin > 65535 || c.VNCPortMax > 65535 {
errs = packersdk.MultiErrorAppend(
errs, fmt.Errorf("vmc_port_min and vnc_port_max must both be below 65535 to be valid TCP ports"))
}
if c.VNCPortMin > c.VNCPortMax {
errs = packersdk.MultiErrorAppend(
errs, fmt.Errorf("vnc_port_min must be less than vnc_port_max"))
}
if c.NetBridge != "" && runtime.GOOS != "linux" {
errs = packersdk.MultiErrorAppend(
errs, fmt.Errorf("net_bridge is only supported in Linux based OSes"))
}
if c.NetBridge != "" || c.VNCUsePassword {
c.QMPEnable = true
}
if c.QMPEnable && c.QMPSocketPath == "" {
socketName := fmt.Sprintf("%s.monitor", c.VMName)
c.QMPSocketPath = filepath.Join(c.OutputDir, socketName)
}
if c.QemuArgs == nil {
c.QemuArgs = make([][]string, 0)
}
if errs != nil && len(errs.Errors) > 0 {
return warnings, errs
}
return warnings, nil
}

View File

@ -0,0 +1,298 @@
// Code generated by "packer-sdc mapstructure-to-hcl2"; DO NOT EDIT.
package qemu
import (
"github.com/hashicorp/hcl/v2/hcldec"
"github.com/zclconf/go-cty/cty"
)
// FlatConfig is an auto-generated flat version of Config.
// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up.
type FlatConfig struct {
PackerBuildName *string `mapstructure:"packer_build_name" cty:"packer_build_name" hcl:"packer_build_name"`
PackerBuilderType *string `mapstructure:"packer_builder_type" cty:"packer_builder_type" hcl:"packer_builder_type"`
PackerCoreVersion *string `mapstructure:"packer_core_version" cty:"packer_core_version" hcl:"packer_core_version"`
PackerDebug *bool `mapstructure:"packer_debug" cty:"packer_debug" hcl:"packer_debug"`
PackerForce *bool `mapstructure:"packer_force" cty:"packer_force" hcl:"packer_force"`
PackerOnError *string `mapstructure:"packer_on_error" cty:"packer_on_error" hcl:"packer_on_error"`
PackerUserVars map[string]string `mapstructure:"packer_user_variables" cty:"packer_user_variables" hcl:"packer_user_variables"`
PackerSensitiveVars []string `mapstructure:"packer_sensitive_variables" cty:"packer_sensitive_variables" hcl:"packer_sensitive_variables"`
HTTPDir *string `mapstructure:"http_directory" cty:"http_directory" hcl:"http_directory"`
HTTPContent map[string]string `mapstructure:"http_content" cty:"http_content" hcl:"http_content"`
HTTPPortMin *int `mapstructure:"http_port_min" cty:"http_port_min" hcl:"http_port_min"`
HTTPPortMax *int `mapstructure:"http_port_max" cty:"http_port_max" hcl:"http_port_max"`
HTTPAddress *string `mapstructure:"http_bind_address" cty:"http_bind_address" hcl:"http_bind_address"`
HTTPInterface *string `mapstructure:"http_interface" undocumented:"true" cty:"http_interface" hcl:"http_interface"`
ISOChecksum *string `mapstructure:"iso_checksum" required:"true" cty:"iso_checksum" hcl:"iso_checksum"`
RawSingleISOUrl *string `mapstructure:"iso_url" required:"true" cty:"iso_url" hcl:"iso_url"`
ISOUrls []string `mapstructure:"iso_urls" cty:"iso_urls" hcl:"iso_urls"`
TargetPath *string `mapstructure:"iso_target_path" cty:"iso_target_path" hcl:"iso_target_path"`
TargetExtension *string `mapstructure:"iso_target_extension" cty:"iso_target_extension" hcl:"iso_target_extension"`
BootGroupInterval *string `mapstructure:"boot_keygroup_interval" cty:"boot_keygroup_interval" hcl:"boot_keygroup_interval"`
BootWait *string `mapstructure:"boot_wait" cty:"boot_wait" hcl:"boot_wait"`
BootCommand []string `mapstructure:"boot_command" cty:"boot_command" hcl:"boot_command"`
DisableVNC *bool `mapstructure:"disable_vnc" cty:"disable_vnc" hcl:"disable_vnc"`
BootKeyInterval *string `mapstructure:"boot_key_interval" cty:"boot_key_interval" hcl:"boot_key_interval"`
ShutdownCommand *string `mapstructure:"shutdown_command" required:"false" cty:"shutdown_command" hcl:"shutdown_command"`
ShutdownTimeout *string `mapstructure:"shutdown_timeout" required:"false" cty:"shutdown_timeout" hcl:"shutdown_timeout"`
Type *string `mapstructure:"communicator" cty:"communicator" hcl:"communicator"`
PauseBeforeConnect *string `mapstructure:"pause_before_connecting" cty:"pause_before_connecting" hcl:"pause_before_connecting"`
SSHHost *string `mapstructure:"ssh_host" cty:"ssh_host" hcl:"ssh_host"`
SSHPort *int `mapstructure:"ssh_port" cty:"ssh_port" hcl:"ssh_port"`
SSHUsername *string `mapstructure:"ssh_username" cty:"ssh_username" hcl:"ssh_username"`
SSHPassword *string `mapstructure:"ssh_password" cty:"ssh_password" hcl:"ssh_password"`
SSHKeyPairName *string `mapstructure:"ssh_keypair_name" undocumented:"true" cty:"ssh_keypair_name" hcl:"ssh_keypair_name"`
SSHTemporaryKeyPairName *string `mapstructure:"temporary_key_pair_name" undocumented:"true" cty:"temporary_key_pair_name" hcl:"temporary_key_pair_name"`
SSHTemporaryKeyPairType *string `mapstructure:"temporary_key_pair_type" cty:"temporary_key_pair_type" hcl:"temporary_key_pair_type"`
SSHTemporaryKeyPairBits *int `mapstructure:"temporary_key_pair_bits" cty:"temporary_key_pair_bits" hcl:"temporary_key_pair_bits"`
SSHCiphers []string `mapstructure:"ssh_ciphers" cty:"ssh_ciphers" hcl:"ssh_ciphers"`
SSHClearAuthorizedKeys *bool `mapstructure:"ssh_clear_authorized_keys" cty:"ssh_clear_authorized_keys" hcl:"ssh_clear_authorized_keys"`
SSHKEXAlgos []string `mapstructure:"ssh_key_exchange_algorithms" cty:"ssh_key_exchange_algorithms" hcl:"ssh_key_exchange_algorithms"`
SSHPrivateKeyFile *string `mapstructure:"ssh_private_key_file" undocumented:"true" cty:"ssh_private_key_file" hcl:"ssh_private_key_file"`
SSHCertificateFile *string `mapstructure:"ssh_certificate_file" cty:"ssh_certificate_file" hcl:"ssh_certificate_file"`
SSHPty *bool `mapstructure:"ssh_pty" cty:"ssh_pty" hcl:"ssh_pty"`
SSHTimeout *string `mapstructure:"ssh_timeout" cty:"ssh_timeout" hcl:"ssh_timeout"`
SSHWaitTimeout *string `mapstructure:"ssh_wait_timeout" undocumented:"true" cty:"ssh_wait_timeout" hcl:"ssh_wait_timeout"`
SSHAgentAuth *bool `mapstructure:"ssh_agent_auth" undocumented:"true" cty:"ssh_agent_auth" hcl:"ssh_agent_auth"`
SSHDisableAgentForwarding *bool `mapstructure:"ssh_disable_agent_forwarding" cty:"ssh_disable_agent_forwarding" hcl:"ssh_disable_agent_forwarding"`
SSHHandshakeAttempts *int `mapstructure:"ssh_handshake_attempts" cty:"ssh_handshake_attempts" hcl:"ssh_handshake_attempts"`
SSHBastionHost *string `mapstructure:"ssh_bastion_host" cty:"ssh_bastion_host" hcl:"ssh_bastion_host"`
SSHBastionPort *int `mapstructure:"ssh_bastion_port" cty:"ssh_bastion_port" hcl:"ssh_bastion_port"`
SSHBastionAgentAuth *bool `mapstructure:"ssh_bastion_agent_auth" cty:"ssh_bastion_agent_auth" hcl:"ssh_bastion_agent_auth"`
SSHBastionUsername *string `mapstructure:"ssh_bastion_username" cty:"ssh_bastion_username" hcl:"ssh_bastion_username"`
SSHBastionPassword *string `mapstructure:"ssh_bastion_password" cty:"ssh_bastion_password" hcl:"ssh_bastion_password"`
SSHBastionInteractive *bool `mapstructure:"ssh_bastion_interactive" cty:"ssh_bastion_interactive" hcl:"ssh_bastion_interactive"`
SSHBastionPrivateKeyFile *string `mapstructure:"ssh_bastion_private_key_file" cty:"ssh_bastion_private_key_file" hcl:"ssh_bastion_private_key_file"`
SSHBastionCertificateFile *string `mapstructure:"ssh_bastion_certificate_file" cty:"ssh_bastion_certificate_file" hcl:"ssh_bastion_certificate_file"`
SSHFileTransferMethod *string `mapstructure:"ssh_file_transfer_method" cty:"ssh_file_transfer_method" hcl:"ssh_file_transfer_method"`
SSHProxyHost *string `mapstructure:"ssh_proxy_host" cty:"ssh_proxy_host" hcl:"ssh_proxy_host"`
SSHProxyPort *int `mapstructure:"ssh_proxy_port" cty:"ssh_proxy_port" hcl:"ssh_proxy_port"`
SSHProxyUsername *string `mapstructure:"ssh_proxy_username" cty:"ssh_proxy_username" hcl:"ssh_proxy_username"`
SSHProxyPassword *string `mapstructure:"ssh_proxy_password" cty:"ssh_proxy_password" hcl:"ssh_proxy_password"`
SSHKeepAliveInterval *string `mapstructure:"ssh_keep_alive_interval" cty:"ssh_keep_alive_interval" hcl:"ssh_keep_alive_interval"`
SSHReadWriteTimeout *string `mapstructure:"ssh_read_write_timeout" cty:"ssh_read_write_timeout" hcl:"ssh_read_write_timeout"`
SSHRemoteTunnels []string `mapstructure:"ssh_remote_tunnels" cty:"ssh_remote_tunnels" hcl:"ssh_remote_tunnels"`
SSHLocalTunnels []string `mapstructure:"ssh_local_tunnels" cty:"ssh_local_tunnels" hcl:"ssh_local_tunnels"`
SSHPublicKey []byte `mapstructure:"ssh_public_key" undocumented:"true" cty:"ssh_public_key" hcl:"ssh_public_key"`
SSHPrivateKey []byte `mapstructure:"ssh_private_key" undocumented:"true" cty:"ssh_private_key" hcl:"ssh_private_key"`
WinRMUser *string `mapstructure:"winrm_username" cty:"winrm_username" hcl:"winrm_username"`
WinRMPassword *string `mapstructure:"winrm_password" cty:"winrm_password" hcl:"winrm_password"`
WinRMHost *string `mapstructure:"winrm_host" cty:"winrm_host" hcl:"winrm_host"`
WinRMNoProxy *bool `mapstructure:"winrm_no_proxy" cty:"winrm_no_proxy" hcl:"winrm_no_proxy"`
WinRMPort *int `mapstructure:"winrm_port" cty:"winrm_port" hcl:"winrm_port"`
WinRMTimeout *string `mapstructure:"winrm_timeout" cty:"winrm_timeout" hcl:"winrm_timeout"`
WinRMUseSSL *bool `mapstructure:"winrm_use_ssl" cty:"winrm_use_ssl" hcl:"winrm_use_ssl"`
WinRMInsecure *bool `mapstructure:"winrm_insecure" cty:"winrm_insecure" hcl:"winrm_insecure"`
WinRMUseNTLM *bool `mapstructure:"winrm_use_ntlm" cty:"winrm_use_ntlm" hcl:"winrm_use_ntlm"`
HostPortMin *int `mapstructure:"host_port_min" required:"false" cty:"host_port_min" hcl:"host_port_min"`
HostPortMax *int `mapstructure:"host_port_max" required:"false" cty:"host_port_max" hcl:"host_port_max"`
SkipNatMapping *bool `mapstructure:"skip_nat_mapping" required:"false" cty:"skip_nat_mapping" hcl:"skip_nat_mapping"`
SSHHostPortMin *int `mapstructure:"ssh_host_port_min" required:"false" cty:"ssh_host_port_min" hcl:"ssh_host_port_min"`
SSHHostPortMax *int `mapstructure:"ssh_host_port_max" cty:"ssh_host_port_max" hcl:"ssh_host_port_max"`
FloppyFiles []string `mapstructure:"floppy_files" cty:"floppy_files" hcl:"floppy_files"`
FloppyDirectories []string `mapstructure:"floppy_dirs" cty:"floppy_dirs" hcl:"floppy_dirs"`
FloppyLabel *string `mapstructure:"floppy_label" cty:"floppy_label" hcl:"floppy_label"`
CDFiles []string `mapstructure:"cd_files" cty:"cd_files" hcl:"cd_files"`
CDLabel *string `mapstructure:"cd_label" cty:"cd_label" hcl:"cd_label"`
ISOSkipCache *bool `mapstructure:"iso_skip_cache" required:"false" cty:"iso_skip_cache" hcl:"iso_skip_cache"`
Accelerator *string `mapstructure:"accelerator" required:"false" cty:"accelerator" hcl:"accelerator"`
AdditionalDiskSize []string `mapstructure:"disk_additional_size" required:"false" cty:"disk_additional_size" hcl:"disk_additional_size"`
CpuCount *int `mapstructure:"cpus" required:"false" cty:"cpus" hcl:"cpus"`
Firmware *string `mapstructure:"firmware" required:"false" cty:"firmware" hcl:"firmware"`
DiskInterface *string `mapstructure:"disk_interface" required:"false" cty:"disk_interface" hcl:"disk_interface"`
DiskSize *string `mapstructure:"disk_size" required:"false" cty:"disk_size" hcl:"disk_size"`
SkipResizeDisk *bool `mapstructure:"skip_resize_disk" required:"false" cty:"skip_resize_disk" hcl:"skip_resize_disk"`
DiskCache *string `mapstructure:"disk_cache" required:"false" cty:"disk_cache" hcl:"disk_cache"`
DiskDiscard *string `mapstructure:"disk_discard" required:"false" cty:"disk_discard" hcl:"disk_discard"`
DetectZeroes *string `mapstructure:"disk_detect_zeroes" required:"false" cty:"disk_detect_zeroes" hcl:"disk_detect_zeroes"`
SkipCompaction *bool `mapstructure:"skip_compaction" required:"false" cty:"skip_compaction" hcl:"skip_compaction"`
DiskCompression *bool `mapstructure:"disk_compression" required:"false" cty:"disk_compression" hcl:"disk_compression"`
Format *string `mapstructure:"format" required:"false" cty:"format" hcl:"format"`
Headless *bool `mapstructure:"headless" required:"false" cty:"headless" hcl:"headless"`
DiskImage *bool `mapstructure:"disk_image" required:"false" cty:"disk_image" hcl:"disk_image"`
UseBackingFile *bool `mapstructure:"use_backing_file" required:"false" cty:"use_backing_file" hcl:"use_backing_file"`
MachineType *string `mapstructure:"machine_type" required:"false" cty:"machine_type" hcl:"machine_type"`
MemorySize *int `mapstructure:"memory" required:"false" cty:"memory" hcl:"memory"`
NetDevice *string `mapstructure:"net_device" required:"false" cty:"net_device" hcl:"net_device"`
NetBridge *string `mapstructure:"net_bridge" required:"false" cty:"net_bridge" hcl:"net_bridge"`
OutputDir *string `mapstructure:"output_directory" required:"false" cty:"output_directory" hcl:"output_directory"`
QemuArgs [][]string `mapstructure:"qemuargs" required:"false" cty:"qemuargs" hcl:"qemuargs"`
QemuImgArgs *FlatQemuImgArgs `mapstructure:"qemu_img_args" required:"false" cty:"qemu_img_args" hcl:"qemu_img_args"`
QemuBinary *string `mapstructure:"qemu_binary" required:"false" cty:"qemu_binary" hcl:"qemu_binary"`
QMPEnable *bool `mapstructure:"qmp_enable" required:"false" cty:"qmp_enable" hcl:"qmp_enable"`
QMPSocketPath *string `mapstructure:"qmp_socket_path" required:"false" cty:"qmp_socket_path" hcl:"qmp_socket_path"`
UseDefaultDisplay *bool `mapstructure:"use_default_display" required:"false" cty:"use_default_display" hcl:"use_default_display"`
Display *string `mapstructure:"display" required:"false" cty:"display" hcl:"display"`
VNCBindAddress *string `mapstructure:"vnc_bind_address" required:"false" cty:"vnc_bind_address" hcl:"vnc_bind_address"`
VNCUsePassword *bool `mapstructure:"vnc_use_password" required:"false" cty:"vnc_use_password" hcl:"vnc_use_password"`
VNCPortMin *int `mapstructure:"vnc_port_min" required:"false" cty:"vnc_port_min" hcl:"vnc_port_min"`
VNCPortMax *int `mapstructure:"vnc_port_max" cty:"vnc_port_max" hcl:"vnc_port_max"`
VMName *string `mapstructure:"vm_name" required:"false" cty:"vm_name" hcl:"vm_name"`
CDROMInterface *string `mapstructure:"cdrom_interface" required:"false" cty:"cdrom_interface" hcl:"cdrom_interface"`
RunOnce *bool `mapstructure:"run_once" cty:"run_once" hcl:"run_once"`
}
// FlatMapstructure returns a new FlatConfig.
// FlatConfig is an auto-generated flat version of Config.
// Where the contents a fields with a `mapstructure:,squash` tag are bubbled up.
func (*Config) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } {
return new(FlatConfig)
}
// HCL2Spec returns the hcl spec of a Config.
// This spec is used by HCL to read the fields of Config.
// The decoded values from this spec will then be applied to a FlatConfig.
func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
s := map[string]hcldec.Spec{
"packer_build_name": &hcldec.AttrSpec{Name: "packer_build_name", Type: cty.String, Required: false},
"packer_builder_type": &hcldec.AttrSpec{Name: "packer_builder_type", Type: cty.String, Required: false},
"packer_core_version": &hcldec.AttrSpec{Name: "packer_core_version", Type: cty.String, Required: false},
"packer_debug": &hcldec.AttrSpec{Name: "packer_debug", Type: cty.Bool, Required: false},
"packer_force": &hcldec.AttrSpec{Name: "packer_force", Type: cty.Bool, Required: false},
"packer_on_error": &hcldec.AttrSpec{Name: "packer_on_error", Type: cty.String, Required: false},
"packer_user_variables": &hcldec.AttrSpec{Name: "packer_user_variables", Type: cty.Map(cty.String), Required: false},
"packer_sensitive_variables": &hcldec.AttrSpec{Name: "packer_sensitive_variables", Type: cty.List(cty.String), Required: false},
"http_directory": &hcldec.AttrSpec{Name: "http_directory", Type: cty.String, Required: false},
"http_content": &hcldec.AttrSpec{Name: "http_content", Type: cty.Map(cty.String), Required: false},
"http_port_min": &hcldec.AttrSpec{Name: "http_port_min", Type: cty.Number, Required: false},
"http_port_max": &hcldec.AttrSpec{Name: "http_port_max", Type: cty.Number, Required: false},
"http_bind_address": &hcldec.AttrSpec{Name: "http_bind_address", Type: cty.String, Required: false},
"http_interface": &hcldec.AttrSpec{Name: "http_interface", Type: cty.String, Required: false},
"iso_checksum": &hcldec.AttrSpec{Name: "iso_checksum", Type: cty.String, Required: false},
"iso_url": &hcldec.AttrSpec{Name: "iso_url", Type: cty.String, Required: false},
"iso_urls": &hcldec.AttrSpec{Name: "iso_urls", Type: cty.List(cty.String), Required: false},
"iso_target_path": &hcldec.AttrSpec{Name: "iso_target_path", Type: cty.String, Required: false},
"iso_target_extension": &hcldec.AttrSpec{Name: "iso_target_extension", Type: cty.String, Required: false},
"boot_keygroup_interval": &hcldec.AttrSpec{Name: "boot_keygroup_interval", Type: cty.String, Required: false},
"boot_wait": &hcldec.AttrSpec{Name: "boot_wait", Type: cty.String, Required: false},
"boot_command": &hcldec.AttrSpec{Name: "boot_command", Type: cty.List(cty.String), Required: false},
"disable_vnc": &hcldec.AttrSpec{Name: "disable_vnc", Type: cty.Bool, Required: false},
"boot_key_interval": &hcldec.AttrSpec{Name: "boot_key_interval", Type: cty.String, Required: false},
"shutdown_command": &hcldec.AttrSpec{Name: "shutdown_command", Type: cty.String, Required: false},
"shutdown_timeout": &hcldec.AttrSpec{Name: "shutdown_timeout", Type: cty.String, Required: false},
"communicator": &hcldec.AttrSpec{Name: "communicator", Type: cty.String, Required: false},
"pause_before_connecting": &hcldec.AttrSpec{Name: "pause_before_connecting", Type: cty.String, Required: false},
"ssh_host": &hcldec.AttrSpec{Name: "ssh_host", Type: cty.String, Required: false},
"ssh_port": &hcldec.AttrSpec{Name: "ssh_port", Type: cty.Number, Required: false},
"ssh_username": &hcldec.AttrSpec{Name: "ssh_username", Type: cty.String, Required: false},
"ssh_password": &hcldec.AttrSpec{Name: "ssh_password", Type: cty.String, Required: false},
"ssh_keypair_name": &hcldec.AttrSpec{Name: "ssh_keypair_name", Type: cty.String, Required: false},
"temporary_key_pair_name": &hcldec.AttrSpec{Name: "temporary_key_pair_name", Type: cty.String, Required: false},
"temporary_key_pair_type": &hcldec.AttrSpec{Name: "temporary_key_pair_type", Type: cty.String, Required: false},
"temporary_key_pair_bits": &hcldec.AttrSpec{Name: "temporary_key_pair_bits", Type: cty.Number, Required: false},
"ssh_ciphers": &hcldec.AttrSpec{Name: "ssh_ciphers", Type: cty.List(cty.String), Required: false},
"ssh_clear_authorized_keys": &hcldec.AttrSpec{Name: "ssh_clear_authorized_keys", Type: cty.Bool, Required: false},
"ssh_key_exchange_algorithms": &hcldec.AttrSpec{Name: "ssh_key_exchange_algorithms", Type: cty.List(cty.String), Required: false},
"ssh_private_key_file": &hcldec.AttrSpec{Name: "ssh_private_key_file", Type: cty.String, Required: false},
"ssh_certificate_file": &hcldec.AttrSpec{Name: "ssh_certificate_file", Type: cty.String, Required: false},
"ssh_pty": &hcldec.AttrSpec{Name: "ssh_pty", Type: cty.Bool, Required: false},
"ssh_timeout": &hcldec.AttrSpec{Name: "ssh_timeout", Type: cty.String, Required: false},
"ssh_wait_timeout": &hcldec.AttrSpec{Name: "ssh_wait_timeout", Type: cty.String, Required: false},
"ssh_agent_auth": &hcldec.AttrSpec{Name: "ssh_agent_auth", Type: cty.Bool, Required: false},
"ssh_disable_agent_forwarding": &hcldec.AttrSpec{Name: "ssh_disable_agent_forwarding", Type: cty.Bool, Required: false},
"ssh_handshake_attempts": &hcldec.AttrSpec{Name: "ssh_handshake_attempts", Type: cty.Number, Required: false},
"ssh_bastion_host": &hcldec.AttrSpec{Name: "ssh_bastion_host", Type: cty.String, Required: false},
"ssh_bastion_port": &hcldec.AttrSpec{Name: "ssh_bastion_port", Type: cty.Number, Required: false},
"ssh_bastion_agent_auth": &hcldec.AttrSpec{Name: "ssh_bastion_agent_auth", Type: cty.Bool, Required: false},
"ssh_bastion_username": &hcldec.AttrSpec{Name: "ssh_bastion_username", Type: cty.String, Required: false},
"ssh_bastion_password": &hcldec.AttrSpec{Name: "ssh_bastion_password", Type: cty.String, Required: false},
"ssh_bastion_interactive": &hcldec.AttrSpec{Name: "ssh_bastion_interactive", Type: cty.Bool, Required: false},
"ssh_bastion_private_key_file": &hcldec.AttrSpec{Name: "ssh_bastion_private_key_file", Type: cty.String, Required: false},
"ssh_bastion_certificate_file": &hcldec.AttrSpec{Name: "ssh_bastion_certificate_file", Type: cty.String, Required: false},
"ssh_file_transfer_method": &hcldec.AttrSpec{Name: "ssh_file_transfer_method", Type: cty.String, Required: false},
"ssh_proxy_host": &hcldec.AttrSpec{Name: "ssh_proxy_host", Type: cty.String, Required: false},
"ssh_proxy_port": &hcldec.AttrSpec{Name: "ssh_proxy_port", Type: cty.Number, Required: false},
"ssh_proxy_username": &hcldec.AttrSpec{Name: "ssh_proxy_username", Type: cty.String, Required: false},
"ssh_proxy_password": &hcldec.AttrSpec{Name: "ssh_proxy_password", Type: cty.String, Required: false},
"ssh_keep_alive_interval": &hcldec.AttrSpec{Name: "ssh_keep_alive_interval", Type: cty.String, Required: false},
"ssh_read_write_timeout": &hcldec.AttrSpec{Name: "ssh_read_write_timeout", Type: cty.String, Required: false},
"ssh_remote_tunnels": &hcldec.AttrSpec{Name: "ssh_remote_tunnels", Type: cty.List(cty.String), Required: false},
"ssh_local_tunnels": &hcldec.AttrSpec{Name: "ssh_local_tunnels", Type: cty.List(cty.String), Required: false},
"ssh_public_key": &hcldec.AttrSpec{Name: "ssh_public_key", Type: cty.List(cty.Number), Required: false},
"ssh_private_key": &hcldec.AttrSpec{Name: "ssh_private_key", Type: cty.List(cty.Number), Required: false},
"winrm_username": &hcldec.AttrSpec{Name: "winrm_username", Type: cty.String, Required: false},
"winrm_password": &hcldec.AttrSpec{Name: "winrm_password", Type: cty.String, Required: false},
"winrm_host": &hcldec.AttrSpec{Name: "winrm_host", Type: cty.String, Required: false},
"winrm_no_proxy": &hcldec.AttrSpec{Name: "winrm_no_proxy", Type: cty.Bool, Required: false},
"winrm_port": &hcldec.AttrSpec{Name: "winrm_port", Type: cty.Number, Required: false},
"winrm_timeout": &hcldec.AttrSpec{Name: "winrm_timeout", Type: cty.String, Required: false},
"winrm_use_ssl": &hcldec.AttrSpec{Name: "winrm_use_ssl", Type: cty.Bool, Required: false},
"winrm_insecure": &hcldec.AttrSpec{Name: "winrm_insecure", Type: cty.Bool, Required: false},
"winrm_use_ntlm": &hcldec.AttrSpec{Name: "winrm_use_ntlm", Type: cty.Bool, Required: false},
"host_port_min": &hcldec.AttrSpec{Name: "host_port_min", Type: cty.Number, Required: false},
"host_port_max": &hcldec.AttrSpec{Name: "host_port_max", Type: cty.Number, Required: false},
"skip_nat_mapping": &hcldec.AttrSpec{Name: "skip_nat_mapping", Type: cty.Bool, Required: false},
"ssh_host_port_min": &hcldec.AttrSpec{Name: "ssh_host_port_min", Type: cty.Number, Required: false},
"ssh_host_port_max": &hcldec.AttrSpec{Name: "ssh_host_port_max", Type: cty.Number, Required: false},
"floppy_files": &hcldec.AttrSpec{Name: "floppy_files", Type: cty.List(cty.String), Required: false},
"floppy_dirs": &hcldec.AttrSpec{Name: "floppy_dirs", Type: cty.List(cty.String), Required: false},
"floppy_label": &hcldec.AttrSpec{Name: "floppy_label", Type: cty.String, Required: false},
"cd_files": &hcldec.AttrSpec{Name: "cd_files", Type: cty.List(cty.String), Required: false},
"cd_label": &hcldec.AttrSpec{Name: "cd_label", Type: cty.String, Required: false},
"iso_skip_cache": &hcldec.AttrSpec{Name: "iso_skip_cache", Type: cty.Bool, Required: false},
"accelerator": &hcldec.AttrSpec{Name: "accelerator", Type: cty.String, Required: false},
"disk_additional_size": &hcldec.AttrSpec{Name: "disk_additional_size", Type: cty.List(cty.String), Required: false},
"cpus": &hcldec.AttrSpec{Name: "cpus", Type: cty.Number, Required: false},
"firmware": &hcldec.AttrSpec{Name: "firmware", Type: cty.String, Required: false},
"disk_interface": &hcldec.AttrSpec{Name: "disk_interface", Type: cty.String, Required: false},
"disk_size": &hcldec.AttrSpec{Name: "disk_size", Type: cty.String, Required: false},
"skip_resize_disk": &hcldec.AttrSpec{Name: "skip_resize_disk", Type: cty.Bool, Required: false},
"disk_cache": &hcldec.AttrSpec{Name: "disk_cache", Type: cty.String, Required: false},
"disk_discard": &hcldec.AttrSpec{Name: "disk_discard", Type: cty.String, Required: false},
"disk_detect_zeroes": &hcldec.AttrSpec{Name: "disk_detect_zeroes", Type: cty.String, Required: false},
"skip_compaction": &hcldec.AttrSpec{Name: "skip_compaction", Type: cty.Bool, Required: false},
"disk_compression": &hcldec.AttrSpec{Name: "disk_compression", Type: cty.Bool, Required: false},
"format": &hcldec.AttrSpec{Name: "format", Type: cty.String, Required: false},
"headless": &hcldec.AttrSpec{Name: "headless", Type: cty.Bool, Required: false},
"disk_image": &hcldec.AttrSpec{Name: "disk_image", Type: cty.Bool, Required: false},
"use_backing_file": &hcldec.AttrSpec{Name: "use_backing_file", Type: cty.Bool, Required: false},
"machine_type": &hcldec.AttrSpec{Name: "machine_type", Type: cty.String, Required: false},
"memory": &hcldec.AttrSpec{Name: "memory", Type: cty.Number, Required: false},
"net_device": &hcldec.AttrSpec{Name: "net_device", Type: cty.String, Required: false},
"net_bridge": &hcldec.AttrSpec{Name: "net_bridge", Type: cty.String, Required: false},
"output_directory": &hcldec.AttrSpec{Name: "output_directory", Type: cty.String, Required: false},
"qemuargs": &hcldec.AttrSpec{Name: "qemuargs", Type: cty.List(cty.List(cty.String)), Required: false},
"qemu_img_args": &hcldec.BlockSpec{TypeName: "qemu_img_args", Nested: hcldec.ObjectSpec((*FlatQemuImgArgs)(nil).HCL2Spec())},
"qemu_binary": &hcldec.AttrSpec{Name: "qemu_binary", Type: cty.String, Required: false},
"qmp_enable": &hcldec.AttrSpec{Name: "qmp_enable", Type: cty.Bool, Required: false},
"qmp_socket_path": &hcldec.AttrSpec{Name: "qmp_socket_path", Type: cty.String, Required: false},
"use_default_display": &hcldec.AttrSpec{Name: "use_default_display", Type: cty.Bool, Required: false},
"display": &hcldec.AttrSpec{Name: "display", Type: cty.String, Required: false},
"vnc_bind_address": &hcldec.AttrSpec{Name: "vnc_bind_address", Type: cty.String, Required: false},
"vnc_use_password": &hcldec.AttrSpec{Name: "vnc_use_password", Type: cty.Bool, Required: false},
"vnc_port_min": &hcldec.AttrSpec{Name: "vnc_port_min", Type: cty.Number, Required: false},
"vnc_port_max": &hcldec.AttrSpec{Name: "vnc_port_max", Type: cty.Number, Required: false},
"vm_name": &hcldec.AttrSpec{Name: "vm_name", Type: cty.String, Required: false},
"cdrom_interface": &hcldec.AttrSpec{Name: "cdrom_interface", Type: cty.String, Required: false},
"run_once": &hcldec.AttrSpec{Name: "run_once", Type: cty.Bool, Required: false},
}
return s
}
// FlatQemuImgArgs is an auto-generated flat version of QemuImgArgs.
// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up.
type FlatQemuImgArgs struct {
Convert []string `mapstructure:"convert" required:"false" cty:"convert" hcl:"convert"`
Create []string `mapstructure:"create" required:"false" cty:"create" hcl:"create"`
Resize []string `mapstructure:"resize" required:"false" cty:"resize" hcl:"resize"`
}
// FlatMapstructure returns a new FlatQemuImgArgs.
// FlatQemuImgArgs is an auto-generated flat version of QemuImgArgs.
// Where the contents a fields with a `mapstructure:,squash` tag are bubbled up.
func (*QemuImgArgs) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } {
return new(FlatQemuImgArgs)
}
// HCL2Spec returns the hcl spec of a QemuImgArgs.
// This spec is used by HCL to read the fields of QemuImgArgs.
// The decoded values from this spec will then be applied to a FlatQemuImgArgs.
func (*FlatQemuImgArgs) HCL2Spec() map[string]hcldec.Spec {
s := map[string]hcldec.Spec{
"convert": &hcldec.AttrSpec{Name: "convert", Type: cty.List(cty.String), Required: false},
"create": &hcldec.AttrSpec{Name: "create", Type: cty.List(cty.String), Required: false},
"resize": &hcldec.AttrSpec{Name: "resize", Type: cty.List(cty.String), Required: false},
}
return s
}

View File

@ -0,0 +1,247 @@
package qemu
import (
"bufio"
"bytes"
"fmt"
"io"
"log"
"os"
"os/exec"
"regexp"
"strings"
"sync"
"syscall"
"time"
"unicode"
"github.com/hashicorp/packer-plugin-sdk/multistep"
)
type DriverCancelCallback func(state multistep.StateBag) bool
// A driver is able to talk to qemu-system-x86_64 and perform certain
// operations with it.
type Driver interface {
// Copy bypasses qemu-img convert and directly copies an image
// that doesn't need converting.
Copy(string, string) error
// Stop stops a running machine, forcefully.
Stop() error
// Qemu executes the given command via qemu-system-x86_64
Qemu(qemuArgs ...string) error
// wait on shutdown of the VM with option to cancel
WaitForShutdown(<-chan struct{}) bool
// Qemu executes the given command via qemu-img
QemuImg(...string) error
// Verify checks to make sure that this driver should function
// properly. If there is any indication the driver can't function,
// this will return an error.
Verify() error
// Version reads the version of Qemu that is installed.
Version() (string, error)
}
type QemuDriver struct {
QemuPath string
QemuImgPath string
vmCmd *exec.Cmd
vmEndCh <-chan int
lock sync.Mutex
}
func (d *QemuDriver) Stop() error {
d.lock.Lock()
defer d.lock.Unlock()
if d.vmCmd != nil {
if err := d.vmCmd.Process.Kill(); err != nil {
return err
}
}
return nil
}
func (d *QemuDriver) Copy(sourceName, targetName string) error {
source, err := os.Open(sourceName)
if err != nil {
err = fmt.Errorf("Error opening iso for copy: %s", err)
return err
}
defer source.Close()
// Create will truncate an existing file
target, err := os.Create(targetName)
if err != nil {
err = fmt.Errorf("Error creating hard drive in output dir: %s", err)
return err
}
defer target.Close()
log.Printf("Copying %s to %s", source.Name(), target.Name())
bytes, err := io.Copy(target, source)
if err != nil {
err = fmt.Errorf("Error copying iso to output dir: %s", err)
return err
}
log.Printf(fmt.Sprintf("Copied %d bytes", bytes))
return nil
}
func (d *QemuDriver) Qemu(qemuArgs ...string) error {
d.lock.Lock()
defer d.lock.Unlock()
if d.vmCmd != nil {
panic("Existing VM state found")
}
stdout_r, stdout_w := io.Pipe()
stderr_r, stderr_w := io.Pipe()
log.Printf("Executing %s: %#v", d.QemuPath, qemuArgs)
cmd := exec.Command(d.QemuPath, qemuArgs...)
cmd.Stdout = stdout_w
cmd.Stderr = stderr_w
err := cmd.Start()
if err != nil {
err = fmt.Errorf("Error starting VM: %s", err)
return err
}
go logReader("Qemu stdout", stdout_r)
go logReader("Qemu stderr", stderr_r)
log.Printf("Started Qemu. Pid: %d", cmd.Process.Pid)
// Wait for Qemu to complete in the background, and mark when its done
endCh := make(chan int, 1)
go func() {
defer stderr_w.Close()
defer stdout_w.Close()
var exitCode int = 0
if err := cmd.Wait(); err != nil {
if exiterr, ok := err.(*exec.ExitError); ok {
// The program has exited with an exit code != 0
if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
exitCode = status.ExitStatus()
} else {
exitCode = 254
}
}
}
endCh <- exitCode
d.lock.Lock()
defer d.lock.Unlock()
d.vmCmd = nil
d.vmEndCh = nil
}()
// Wait at least a couple seconds for an early fail from Qemu so
// we can report that.
select {
case exit := <-endCh:
if exit != 0 {
return fmt.Errorf("Qemu failed to start. Please run with PACKER_LOG=1 to get more info.")
}
case <-time.After(2 * time.Second):
}
// Setup our state so we know we are running
d.vmCmd = cmd
d.vmEndCh = endCh
return nil
}
func (d *QemuDriver) WaitForShutdown(cancelCh <-chan struct{}) bool {
d.lock.Lock()
endCh := d.vmEndCh
d.lock.Unlock()
if endCh == nil {
return true
}
select {
case <-endCh:
return true
case <-cancelCh:
return false
}
}
func (d *QemuDriver) QemuImg(args ...string) error {
var stdout, stderr bytes.Buffer
log.Printf("Executing qemu-img: %#v", args)
cmd := exec.Command(d.QemuImgPath, args...)
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
stdoutString := strings.TrimSpace(stdout.String())
stderrString := strings.TrimSpace(stderr.String())
if _, ok := err.(*exec.ExitError); ok {
err = fmt.Errorf("QemuImg error: %s", stderrString)
}
log.Printf("stdout: %s", stdoutString)
log.Printf("stderr: %s", stderrString)
return err
}
func (d *QemuDriver) Verify() error {
return nil
}
func (d *QemuDriver) Version() (string, error) {
var stdout bytes.Buffer
cmd := exec.Command(d.QemuPath, "-version")
cmd.Stdout = &stdout
if err := cmd.Run(); err != nil {
return "", err
}
versionOutput := strings.TrimSpace(stdout.String())
log.Printf("Qemu --version output: %s", versionOutput)
versionRe := regexp.MustCompile(`[\.[0-9]+]*`)
matches := versionRe.FindStringSubmatch(versionOutput)
if len(matches) == 0 {
return "", fmt.Errorf("No version found: %s", versionOutput)
}
log.Printf("Qemu version: %s", matches[0])
return matches[0], nil
}
func logReader(name string, r io.Reader) {
bufR := bufio.NewReader(r)
for {
line, err := bufR.ReadString('\n')
if line != "" {
line = strings.TrimRightFunc(line, unicode.IsSpace)
log.Printf("%s: %s", name, line)
}
if err == io.EOF {
break
}
}
}

View File

@ -0,0 +1,74 @@
package qemu
import "sync"
type DriverMock struct {
sync.Mutex
CopyCalled bool
CopyErr error
StopCalled bool
StopErr error
QemuCalls [][]string
QemuErrs []error
WaitForShutdownCalled bool
WaitForShutdownState bool
QemuImgCalled bool
QemuImgCalls []string
QemuImgErrs []error
VerifyCalled bool
VerifyErr error
VersionCalled bool
VersionResult string
VersionErr error
}
func (d *DriverMock) Copy(source, dst string) error {
d.CopyCalled = true
return d.CopyErr
}
func (d *DriverMock) Stop() error {
d.StopCalled = true
return d.StopErr
}
func (d *DriverMock) Qemu(args ...string) error {
d.QemuCalls = append(d.QemuCalls, args)
if len(d.QemuErrs) >= len(d.QemuCalls) {
return d.QemuErrs[len(d.QemuCalls)-1]
}
return nil
}
func (d *DriverMock) WaitForShutdown(cancelCh <-chan struct{}) bool {
d.WaitForShutdownCalled = true
return d.WaitForShutdownState
}
func (d *DriverMock) QemuImg(args ...string) error {
d.QemuImgCalled = true
d.QemuImgCalls = append(d.QemuImgCalls, args...)
if len(d.QemuImgErrs) >= len(d.QemuImgCalls) {
return d.QemuImgErrs[len(d.QemuImgCalls)-1]
}
return nil
}
func (d *DriverMock) Verify() error {
d.VerifyCalled = true
return d.VerifyErr
}
func (d *DriverMock) Version() (string, error) {
d.VersionCalled = true
return d.VersionResult, d.VersionErr
}

View File

@ -0,0 +1,135 @@
package qemu
import (
"encoding/json"
"fmt"
"strings"
"github.com/digitalocean/go-qemu/qmp"
)
type qomListRequest struct {
Execute string `json:"execute"`
Arguments qomListRequestArguments `json:"arguments"`
}
type qomListRequestArguments struct {
Path string `json:"path"`
}
type qomListResponse struct {
Return []qomListReturn `json:"return"`
}
type qomListReturn struct {
Name string `json:"name"`
Type string `json:"type"`
}
func qmpQomList(qmpMonitor *qmp.SocketMonitor, path string) ([]qomListReturn, error) {
request, _ := json.Marshal(qomListRequest{
Execute: "qom-list",
Arguments: qomListRequestArguments{
Path: path,
},
})
result, err := qmpMonitor.Run(request)
if err != nil {
return nil, err
}
var response qomListResponse
if err := json.Unmarshal(result, &response); err != nil {
return nil, err
}
return response.Return, nil
}
type qomGetRequest struct {
Execute string `json:"execute"`
Arguments qomGetRequestArguments `json:"arguments"`
}
type qomGetRequestArguments struct {
Path string `json:"path"`
Property string `json:"property"`
}
type qomGetResponse struct {
Return string `json:"return"`
}
func qmpQomGet(qmpMonitor *qmp.SocketMonitor, path string, property string) (string, error) {
request, _ := json.Marshal(qomGetRequest{
Execute: "qom-get",
Arguments: qomGetRequestArguments{
Path: path,
Property: property,
},
})
result, err := qmpMonitor.Run(request)
if err != nil {
return "", err
}
var response qomGetResponse
if err := json.Unmarshal(result, &response); err != nil {
return "", err
}
return response.Return, nil
}
type netDevice struct {
Path string
Name string
Type string
MacAddress string
}
func getNetDevices(qmpMonitor *qmp.SocketMonitor) ([]netDevice, error) {
devices := []netDevice{}
for _, parentPath := range []string{"/machine/peripheral", "/machine/peripheral-anon"} {
listResponse, err := qmpQomList(qmpMonitor, parentPath)
if err != nil {
return nil, fmt.Errorf("failed to get qmp qom list %v: %w", parentPath, err)
}
for _, p := range listResponse {
if strings.HasPrefix(p.Type, "child<") {
path := fmt.Sprintf("%s/%s", parentPath, p.Name)
r, err := qmpQomList(qmpMonitor, path)
if err != nil {
return nil, fmt.Errorf("failed to get qmp qom list %v: %w", path, err)
}
isNetdev := false
for _, d := range r {
if d.Name == "netdev" {
isNetdev = true
break
}
}
if isNetdev {
device := netDevice{
Path: path,
}
for _, d := range r {
if d.Name != "type" && d.Name != "netdev" && d.Name != "mac" {
continue
}
value, err := qmpQomGet(qmpMonitor, path, d.Name)
if err != nil {
return nil, fmt.Errorf("failed to get qmp qom property %v %v: %w", path, d.Name, err)
}
switch d.Name {
case "type":
device.Type = value
case "netdev":
device.Name = value
case "mac":
device.MacAddress = value
}
}
devices = append(devices, device)
}
}
}
}
return devices, nil
}

View File

@ -0,0 +1,30 @@
package qemu
import (
"log"
"github.com/hashicorp/packer-plugin-sdk/multistep"
)
func commHost(host string) func(multistep.StateBag) (string, error) {
return func(state multistep.StateBag) (string, error) {
if host != "" {
log.Printf("Using host value: %s", host)
return host, nil
}
if guestAddress, ok := state.Get("guestAddress").(string); ok {
return guestAddress, nil
}
return "127.0.0.1", nil
}
}
func commPort(state multistep.StateBag) (int, error) {
commHostPort, ok := state.Get("commHostPort").(int)
if !ok {
commHostPort = 22
}
return commHostPort, nil
}

View File

@ -0,0 +1,92 @@
package qemu
import (
"context"
"fmt"
"log"
"os"
"time"
"github.com/digitalocean/go-qemu/qmp"
"github.com/hashicorp/packer-plugin-sdk/multistep"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
)
// This step configures the VM to enable the QMP listener.
//
// Uses:
// config *config
// ui packersdk.Ui
//
// Produces:
type stepConfigureQMP struct {
monitor *qmp.SocketMonitor
QMPSocketPath string
}
func (s *stepConfigureQMP) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*Config)
ui := state.Get("ui").(packersdk.Ui)
if !config.QMPEnable {
return multistep.ActionContinue
}
ui.Say(fmt.Sprintf("QMP socket at: %s", s.QMPSocketPath))
// Only initialize and open QMP when we have a use for it.
// Open QMP socket
var err error
var cmd []byte
var result []byte
s.monitor, err = qmp.NewSocketMonitor("unix", s.QMPSocketPath, 2*time.Second)
if err != nil {
err := fmt.Errorf("Error opening QMP socket: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
// Connect to QMP
// function automatically calls capabilities so is immediately ready for commands
err = s.monitor.Connect()
if err != nil {
err := fmt.Errorf("Error connecting to QMP socket: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
log.Printf("QMP socket open SUCCESS")
vncPassword := state.Get("vnc_password")
if vncPassword != "" {
cmd = []byte(fmt.Sprintf("{ \"execute\": \"change-vnc-password\", \"arguments\": { \"password\": \"%s\" } }",
vncPassword))
result, err = s.monitor.Run(cmd)
if err != nil {
err := fmt.Errorf("Error connecting to QMP socket: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
log.Printf("QMP Command: %s\nResult: %s", cmd, result)
}
// make the qmp_monitor available to other steps.
state.Put("qmp_monitor", s.monitor)
return multistep.ActionContinue
}
func (s *stepConfigureQMP) Cleanup(multistep.StateBag) {
if s.monitor != nil {
err := s.monitor.Disconnect()
if err != nil {
log.Printf("failed to disconnect QMP: %v", err)
}
// Delete file associated with qmp socket.
if err := os.Remove(s.QMPSocketPath); err != nil {
log.Printf("Failed to delete the qmp socket file: %s", err)
}
}
}

View File

@ -0,0 +1,89 @@
package qemu
import (
"context"
"fmt"
"log"
"math/rand"
"github.com/hashicorp/packer-plugin-sdk/multistep"
"github.com/hashicorp/packer-plugin-sdk/net"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
)
// This step configures the VM to enable the VNC server.
//
// Uses:
// config *config
// ui packersdk.Ui
//
// Produces:
// vnc_port int - The port that VNC is configured to listen on.
type stepConfigureVNC struct {
l *net.Listener
}
func VNCPassword() string {
length := int(8)
charSet := []byte("012345689abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
charSetLength := len(charSet)
password := make([]byte, length)
for i := 0; i < length; i++ {
password[i] = charSet[rand.Intn(charSetLength)]
}
return string(password)
}
func (s *stepConfigureVNC) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*Config)
ui := state.Get("ui").(packersdk.Ui)
// Find an open VNC port. Note that this can still fail later on
// because we have to release the port at some point. But this does its
// best.
msg := fmt.Sprintf("Looking for available port between %d and %d on %s", config.VNCPortMin, config.VNCPortMax, config.VNCBindAddress)
ui.Say(msg)
log.Print(msg)
var vncPassword string
var err error
s.l, err = net.ListenRangeConfig{
Addr: config.VNCBindAddress,
Min: config.VNCPortMin,
Max: config.VNCPortMax,
Network: "tcp",
}.Listen(ctx)
if err != nil {
err := fmt.Errorf("Error finding VNC port: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
s.l.Listener.Close() // free port, but don't unlock lock file
vncPort := s.l.Port
if config.VNCUsePassword {
vncPassword = VNCPassword()
} else {
vncPassword = ""
}
log.Printf("Found available VNC port: %d on IP: %s", vncPort, config.VNCBindAddress)
state.Put("vnc_port", vncPort)
state.Put("vnc_password", vncPassword)
return multistep.ActionContinue
}
func (s *stepConfigureVNC) Cleanup(multistep.StateBag) {
if s.l != nil {
err := s.l.Close()
if err != nil {
log.Printf("failed to unlock port lockfile: %v", err)
}
}
}

View File

@ -0,0 +1,104 @@
package qemu
import (
"context"
"fmt"
"path/filepath"
"strings"
"time"
"github.com/hashicorp/packer-plugin-sdk/multistep"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
"github.com/hashicorp/packer-plugin-sdk/retry"
"os"
)
// This step converts the virtual disk that was used as the
// hard drive for the virtual machine.
type stepConvertDisk struct {
DiskCompression bool
Format string
OutputDir string
SkipCompaction bool
VMName string
QemuImgArgs QemuImgArgs
}
func (s *stepConvertDisk) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
driver := state.Get("driver").(Driver)
ui := state.Get("ui").(packersdk.Ui)
diskName := s.VMName
if s.SkipCompaction && !s.DiskCompression {
return multistep.ActionContinue
}
name := diskName + ".convert"
sourcePath := filepath.Join(s.OutputDir, diskName)
targetPath := filepath.Join(s.OutputDir, name)
command := s.buildConvertCommand(sourcePath, targetPath)
ui.Say("Converting hard drive...")
// Retry the conversion a few times in case it takes the qemu process a
// moment to release the lock
err := retry.Config{
Tries: 10,
ShouldRetry: func(err error) bool {
if strings.Contains(err.Error(), `Failed to get shared "write" lock`) {
ui.Say("Error getting file lock for conversion; retrying...")
return true
}
return false
},
RetryDelay: (&retry.Backoff{InitialBackoff: 1 * time.Second, MaxBackoff: 10 * time.Second, Multiplier: 2}).Linear,
}.Run(ctx, func(ctx context.Context) error {
return driver.QemuImg(command...)
})
if err != nil {
switch err.(type) {
case *retry.RetryExhaustedError:
err = fmt.Errorf("Exhausted retries for getting file lock: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
default:
err := fmt.Errorf("Error converting hard drive: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
}
if err := os.Rename(targetPath, sourcePath); err != nil {
err := fmt.Errorf("Error moving converted hard drive: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
return multistep.ActionContinue
}
func (s *stepConvertDisk) buildConvertCommand(sourcePath, targetPath string) []string {
command := []string{"convert"}
if s.DiskCompression {
command = append(command, "-c")
}
// Add user-provided convert args
command = append(command, s.QemuImgArgs.Convert...)
// Add format, and paths.
command = append(command, "-O", s.Format, sourcePath, targetPath)
return command
}
func (s *stepConvertDisk) Cleanup(state multistep.StateBag) {}

View File

@ -0,0 +1,78 @@
package qemu
import (
"context"
"fmt"
"path/filepath"
"github.com/hashicorp/packer-plugin-sdk/multistep"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
)
// This step copies the virtual disk that will be used as the
// hard drive for the virtual machine.
type stepCopyDisk struct {
DiskImage bool
Format string
OutputDir string
UseBackingFile bool
VMName string
QemuImgArgs QemuImgArgs
}
func (s *stepCopyDisk) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
driver := state.Get("driver").(Driver)
isoPath := state.Get("iso_path").(string)
ui := state.Get("ui").(packersdk.Ui)
path := filepath.Join(s.OutputDir, s.VMName)
// Only make a copy of the ISO as the 'main' or 'default' disk if is a disk
// image and not using a backing file. The create disk step (step_create_disk.go)
// would already have made the disk otherwise
if !s.DiskImage || s.UseBackingFile {
return multistep.ActionContinue
}
// In some cases, the file formats provided are equivalent by comparing the
// file extensions. Skip the conversion step
// This also serves as a workaround for a QEMU bug: https://bugs.launchpad.net/qemu/+bug/1776920
ext := filepath.Ext(isoPath)
if len(ext) >= 1 && ext[1:] == s.Format && len(s.QemuImgArgs.Convert) == 0 {
ui.Message("File extension already matches desired output format. " +
"Skipping qemu-img convert step")
err := driver.Copy(isoPath, path)
if err != nil {
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
return multistep.ActionContinue
}
command := s.buildConvertCommand(isoPath, path)
ui.Say("Copying hard drive...")
if err := driver.QemuImg(command...); err != nil {
err := fmt.Errorf("Error creating hard drive: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
return multistep.ActionContinue
}
func (s *stepCopyDisk) buildConvertCommand(sourcePath, targetPath string) []string {
command := []string{"convert"}
// Add user-provided convert args
command = append(command, s.QemuImgArgs.Convert...)
// Add format, and paths.
command = append(command, "-O", s.Format, sourcePath, targetPath)
return command
}
func (s *stepCopyDisk) Cleanup(state multistep.StateBag) {}

View File

@ -0,0 +1,91 @@
package qemu
import (
"context"
"fmt"
"log"
"path/filepath"
"github.com/hashicorp/packer-plugin-sdk/multistep"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
)
// This step creates the virtual disk that will be used as the
// hard drive for the virtual machine.
type stepCreateDisk struct {
AdditionalDiskSize []string
DiskImage bool
DiskSize string
Format string
OutputDir string
UseBackingFile bool
VMName string
QemuImgArgs QemuImgArgs
}
func (s *stepCreateDisk) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
driver := state.Get("driver").(Driver)
ui := state.Get("ui").(packersdk.Ui)
name := s.VMName
if len(s.AdditionalDiskSize) > 0 || s.UseBackingFile {
ui.Say("Creating required virtual machine disks")
}
// The 'main' or 'default' disk
diskFullPaths := []string{filepath.Join(s.OutputDir, name)}
diskSizes := []string{s.DiskSize}
// Additional disks
if len(s.AdditionalDiskSize) > 0 {
for i, diskSize := range s.AdditionalDiskSize {
path := filepath.Join(s.OutputDir, fmt.Sprintf("%s-%d", name, i+1))
diskFullPaths = append(diskFullPaths, path)
diskSizes = append(diskSizes, diskSize)
}
}
// Create all required disks
for i, diskFullPath := range diskFullPaths {
if s.DiskImage && !s.UseBackingFile && i == 0 {
// Let the copy disk step (step_copy_disk.go) create the 'main' or
// 'default' disk.
continue
}
log.Printf("[INFO] Creating disk with Path: %s and Size: %s", diskFullPath, diskSizes[i])
command := s.buildCreateCommand(diskFullPath, diskSizes[i], i, state)
if err := driver.QemuImg(command...); err != nil {
err := fmt.Errorf("Error creating hard drive: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
}
// Stash the disk paths so we can retrieve later
state.Put("qemu_disk_paths", diskFullPaths)
return multistep.ActionContinue
}
func (s *stepCreateDisk) buildCreateCommand(path string, size string, i int, state multistep.StateBag) []string {
command := []string{"create", "-f", s.Format}
if s.DiskImage && s.UseBackingFile && i == 0 {
// Use a backing file for the 'main' or 'default' disk
isoPath := state.Get("iso_path").(string)
command = append(command, "-b", isoPath)
}
// add user-provided convert args
command = append(command, s.QemuImgArgs.Create...)
// add target path and size.
command = append(command, path, size)
return command
}
func (s *stepCreateDisk) Cleanup(state multistep.StateBag) {}

View File

@ -0,0 +1,68 @@
package qemu
import (
"context"
"fmt"
"net"
"github.com/hashicorp/packer-plugin-sdk/multistep"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
)
// Step to discover the http ip
// which guests use to reach the vm host
// To make sure the IP is set before boot command and http server steps
type stepHTTPIPDiscover struct{}
func (s *stepHTTPIPDiscover) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*Config)
ui := state.Get("ui").(packersdk.Ui)
hostIP := ""
if config.NetBridge == "" {
hostIP = "10.0.2.2"
} else {
bridgeInterface, err := net.InterfaceByName(config.NetBridge)
if err != nil {
err := fmt.Errorf("Error getting the bridge %s interface: %s", config.NetBridge, err)
ui.Error(err.Error())
return multistep.ActionHalt
}
addrs, err := bridgeInterface.Addrs()
if err != nil {
err := fmt.Errorf("Error getting the bridge %s interface addresses: %s", config.NetBridge, err)
ui.Error(err.Error())
return multistep.ActionHalt
}
for _, addr := range addrs {
var ip net.IP
switch v := addr.(type) {
case *net.IPNet:
ip = v.IP
case *net.IPAddr:
ip = v.IP
}
if ip == nil {
continue
}
ip = ip.To4()
if ip == nil {
continue
}
hostIP = ip.String()
break
}
if hostIP == "" {
err := fmt.Errorf("Error getting an IPv4 address from the bridge %s: cannot find any IPv4 address", config.NetBridge)
ui.Error(err.Error())
return multistep.ActionHalt
}
}
state.Put("http_ip", hostIP)
return multistep.ActionContinue
}
func (s *stepHTTPIPDiscover) Cleanup(state multistep.StateBag) {}

View File

@ -0,0 +1,74 @@
package qemu
import (
"context"
"fmt"
"log"
"github.com/hashicorp/packer-plugin-sdk/multistep"
"github.com/hashicorp/packer-plugin-sdk/net"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
)
// This step adds a NAT port forwarding definition so that SSH or WinRM is available
// on the guest machine.
type stepPortForward struct {
CommunicatorType string
NetBridge string
l *net.Listener
}
func (s *stepPortForward) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*Config)
ui := state.Get("ui").(packersdk.Ui)
if s.CommunicatorType == "none" {
ui.Message("No communicator is set; skipping port forwarding setup.")
return multistep.ActionContinue
}
if s.NetBridge != "" {
ui.Message("net_bridge is set; skipping port forwarding setup.")
return multistep.ActionContinue
}
commHostPort := config.CommConfig.Comm.Port()
if config.CommConfig.SkipNatMapping {
log.Printf("Skipping NAT port forwarding. Using communicator (SSH, WinRM, etc) port %d", commHostPort)
state.Put("commHostPort", commHostPort)
return multistep.ActionContinue
}
log.Printf("Looking for available communicator (SSH, WinRM, etc) port between %d and %d", config.CommConfig.HostPortMin, config.CommConfig.HostPortMax)
var err error
s.l, err = net.ListenRangeConfig{
Addr: config.VNCBindAddress,
Min: config.CommConfig.HostPortMin,
Max: config.CommConfig.HostPortMax,
Network: "tcp",
}.Listen(ctx)
if err != nil {
err := fmt.Errorf("Error finding port: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
s.l.Listener.Close() // free port, but don't unlock lock file
commHostPort = s.l.Port
ui.Say(fmt.Sprintf("Found port for communicator (SSH, WinRM, etc): %d.", commHostPort))
// Save the port we're using so that future steps can use it
state.Put("commHostPort", commHostPort)
return multistep.ActionContinue
}
func (s *stepPortForward) Cleanup(state multistep.StateBag) {
if s.l != nil {
err := s.l.Close()
if err != nil {
log.Printf("failed to unlock port lockfile: %v", err)
}
}
}

View File

@ -0,0 +1,51 @@
package qemu
import (
"context"
"log"
"os"
"time"
"github.com/hashicorp/packer-plugin-sdk/multistep"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
)
type stepPrepareOutputDir struct{}
func (stepPrepareOutputDir) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*Config)
ui := state.Get("ui").(packersdk.Ui)
if _, err := os.Stat(config.OutputDir); err == nil && config.PackerForce {
ui.Say("Deleting previous output directory...")
os.RemoveAll(config.OutputDir)
}
if err := os.MkdirAll(config.OutputDir, 0755); err != nil {
state.Put("error", err)
return multistep.ActionHalt
}
return multistep.ActionContinue
}
func (stepPrepareOutputDir) Cleanup(state multistep.StateBag) {
_, cancelled := state.GetOk(multistep.StateCancelled)
_, halted := state.GetOk(multistep.StateHalted)
if cancelled || halted {
config := state.Get("config").(*Config)
ui := state.Get("ui").(packersdk.Ui)
ui.Say("Deleting output directory...")
for i := 0; i < 5; i++ {
err := os.RemoveAll(config.OutputDir)
if err == nil {
break
}
log.Printf("Error removing output dir: %s", err)
time.Sleep(2 * time.Second)
}
}
}

View File

@ -0,0 +1,60 @@
package qemu
import (
"context"
"fmt"
"path/filepath"
"github.com/hashicorp/packer-plugin-sdk/multistep"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
)
// This step resizes the virtual disk that will be used as the
// hard drive for the virtual machine.
type stepResizeDisk struct {
DiskCompression bool
DiskImage bool
Format string
OutputDir string
SkipResizeDisk bool
VMName string
DiskSize string
QemuImgArgs QemuImgArgs
}
func (s *stepResizeDisk) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
driver := state.Get("driver").(Driver)
ui := state.Get("ui").(packersdk.Ui)
path := filepath.Join(s.OutputDir, s.VMName)
command := s.buildResizeCommand(path)
if s.DiskImage == false || s.SkipResizeDisk == true {
return multistep.ActionContinue
}
ui.Say("Resizing hard drive...")
if err := driver.QemuImg(command...); err != nil {
err := fmt.Errorf("Error creating hard drive: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
return multistep.ActionContinue
}
func (s *stepResizeDisk) buildResizeCommand(path string) []string {
command := []string{"resize", "-f", s.Format}
// add user-provided convert args
command = append(command, s.QemuImgArgs.Resize...)
// Add file and size
command = append(command, path, s.DiskSize)
return command
}
func (s *stepResizeDisk) Cleanup(state multistep.StateBag) {}

View File

@ -0,0 +1,400 @@
package qemu
import (
"context"
"fmt"
"log"
"path/filepath"
"strings"
"github.com/hashicorp/go-version"
"github.com/hashicorp/packer-plugin-sdk/multistep"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
"github.com/hashicorp/packer-plugin-sdk/template/interpolate"
)
// stepRun runs the virtual machine
type stepRun struct {
DiskImage bool
atLeastVersion2 bool
ui packersdk.Ui
}
func (s *stepRun) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*Config)
driver := state.Get("driver").(Driver)
s.ui = state.Get("ui").(packersdk.Ui)
// Figure out version of qemu; store on step for later use
rawVersion, err := driver.Version()
if err != nil {
err := fmt.Errorf("Error determining qemu version: %s", err)
s.ui.Error(err.Error())
return multistep.ActionHalt
}
qemuVersion, err := version.NewVersion(rawVersion)
if err != nil {
err := fmt.Errorf("Error parsing qemu version: %s", err)
s.ui.Error(err.Error())
return multistep.ActionHalt
}
v2 := version.Must(version.NewVersion("2.0"))
s.atLeastVersion2 = qemuVersion.GreaterThanOrEqual(v2)
// Generate the qemu command
command, err := s.getCommandArgs(config, state)
if err != nil {
err := fmt.Errorf("Error processing QemuArgs: %s", err)
s.ui.Error(err.Error())
return multistep.ActionHalt
}
// run the qemu command
if err := driver.Qemu(command...); err != nil {
err := fmt.Errorf("Error launching VM: %s", err)
s.ui.Error(err.Error())
return multistep.ActionHalt
}
return multistep.ActionContinue
}
func (s *stepRun) Cleanup(state multistep.StateBag) {
driver := state.Get("driver").(Driver)
ui := state.Get("ui").(packersdk.Ui)
if err := driver.Stop(); err != nil {
ui.Error(fmt.Sprintf("Error shutting down VM: %s", err))
}
}
func (s *stepRun) getDefaultArgs(config *Config, state multistep.StateBag) map[string]interface{} {
defaultArgs := make(map[string]interface{})
// Configure "boot" arguement
// Run command is different depending whether we're booting from an
// installation CD or a pre-baked image
bootDrive := "once=d"
message := "Starting VM, booting from CD-ROM"
if s.DiskImage {
bootDrive = "c"
message = "Starting VM, booting disk image"
}
s.ui.Say(message)
defaultArgs["-boot"] = bootDrive
// configure "-qmp" arguments
if config.QMPEnable {
defaultArgs["-qmp"] = fmt.Sprintf("unix:%s,server,nowait", config.QMPSocketPath)
}
// configure "-name" arguments
defaultArgs["-name"] = config.VMName
// Configure "-machine" arguments
if config.Accelerator == "none" {
defaultArgs["-machine"] = fmt.Sprintf("type=%s", config.MachineType)
s.ui.Message("WARNING: The VM will be started with no hardware acceleration.\n" +
"The installation may take considerably longer to finish.\n")
} else {
defaultArgs["-machine"] = fmt.Sprintf("type=%s,accel=%s",
config.MachineType, config.Accelerator)
}
// Firmware
if config.Firmware != "" {
defaultArgs["-bios"] = config.Firmware
}
// Configure "-netdev" arguments
defaultArgs["-netdev"] = fmt.Sprintf("bridge,id=user.0,br=%s", config.NetBridge)
if config.NetBridge == "" {
defaultArgs["-netdev"] = fmt.Sprintf("user,id=user.0")
if config.CommConfig.Comm.Type != "none" {
commHostPort := state.Get("commHostPort").(int)
defaultArgs["-netdev"] = fmt.Sprintf("user,id=user.0,hostfwd=tcp::%v-:%d", commHostPort, config.CommConfig.Comm.Port())
}
}
// Configure "-vnc" arguments
// vncPort is always set in stepConfigureVNC, so we don't need to
// defensively assert
vncPort := state.Get("vnc_port").(int)
vncIP := config.VNCBindAddress
vncRealAddress := fmt.Sprintf("%s:%d", vncIP, vncPort)
vncPort = vncPort - 5900
vncArgs := fmt.Sprintf("%s:%d", vncIP, vncPort)
if config.VNCUsePassword {
vncArgs = fmt.Sprintf("%s:%d,password", vncIP, vncPort)
}
defaultArgs["-vnc"] = vncArgs
// Track the connection for the user
vncPass, _ := state.Get("vnc_password").(string)
message = getVncConnectionMessage(config.Headless, vncRealAddress, vncPass)
if message != "" {
s.ui.Message(message)
}
// Configure "-m" memory argument
defaultArgs["-m"] = fmt.Sprintf("%dM", config.MemorySize)
// Configure "-smp" processor hardware arguments
if config.CpuCount > 1 {
defaultArgs["-smp"] = fmt.Sprintf("cpus=%d,sockets=%d", config.CpuCount, config.CpuCount)
}
// Configure "-fda" floppy disk attachment
if floppyPathRaw, ok := state.GetOk("floppy_path"); ok {
defaultArgs["-fda"] = floppyPathRaw.(string)
} else {
log.Println("Qemu Builder has no floppy files, not attaching a floppy.")
}
// Configure GUI display
if !config.Headless {
if s.atLeastVersion2 {
// FIXME: "none" is a valid display option in qemu but we have
// departed from the qemu usage here to instaed mean "let qemu
// set a reasonable default". We need to deprecate this behavior
// and let users just set "UseDefaultDisplay" if they want to let
// qemu do its thing.
if len(config.Display) > 0 && config.Display != "none" {
defaultArgs["-display"] = config.Display
} else if !config.UseDefaultDisplay {
defaultArgs["-display"] = "gtk"
}
} else {
s.ui.Message("WARNING: The version of qemu on your host doesn't support display mode.\n" +
"The display parameter will be ignored.")
}
}
deviceArgs, driveArgs := s.getDeviceAndDriveArgs(config, state)
defaultArgs["-device"] = deviceArgs
defaultArgs["-drive"] = driveArgs
return defaultArgs
}
func getVncConnectionMessage(headless bool, vnc string, vncPass string) string {
// Configure GUI display
if headless {
if vnc == "" {
return "The VM will be run headless, without a GUI, as configured.\n" +
"If the run isn't succeeding as you expect, please enable the GUI\n" +
"to inspect the progress of the build."
}
if vncPass != "" {
return fmt.Sprintf(
"The VM will be run headless, without a GUI. If you want to\n"+
"view the screen of the VM, connect via VNC to vnc://%s\n"+
"with the password: %s", vnc, vncPass)
}
return fmt.Sprintf(
"The VM will be run headless, without a GUI. If you want to\n"+
"view the screen of the VM, connect via VNC without a password to\n"+
"vnc://%s", vnc)
}
return ""
}
func (s *stepRun) getDeviceAndDriveArgs(config *Config, state multistep.StateBag) ([]string, []string) {
var deviceArgs []string
var driveArgs []string
vmName := config.VMName
imgPath := filepath.Join(config.OutputDir, vmName)
// Configure virtual hard drives
if s.atLeastVersion2 {
drivesToAttach := []string{}
if v, ok := state.GetOk("qemu_disk_paths"); ok {
diskFullPaths := v.([]string)
drivesToAttach = append(drivesToAttach, diskFullPaths...)
}
for i, drivePath := range drivesToAttach {
driveArgumentString := fmt.Sprintf("file=%s,if=%s,cache=%s,discard=%s,format=%s", drivePath, config.DiskInterface, config.DiskCache, config.DiskDiscard, config.Format)
if config.DiskInterface == "virtio-scsi" {
// TODO: Megan: Remove this conditional. This, and the code
// under the TODO below, reproduce the old behavior. While it
// may be broken, the goal of this commit is to refactor in a way
// that creates a result that is testably the same as the old
// code. A pr will follow fixing this broken behavior.
if i == 0 {
deviceArgs = append(deviceArgs, fmt.Sprintf("virtio-scsi-pci,id=scsi%d", i))
}
// TODO: Megan: When you remove above conditional,
// set deviceArgs = append(deviceArgs, fmt.Sprintf("scsi-hd,bus=scsi%d.0,drive=drive%d", i, i))
deviceArgs = append(deviceArgs, fmt.Sprintf("scsi-hd,bus=scsi0.0,drive=drive%d", i))
driveArgumentString = fmt.Sprintf("if=none,file=%s,id=drive%d,cache=%s,discard=%s,format=%s", drivePath, i, config.DiskCache, config.DiskDiscard, config.Format)
}
if config.DetectZeroes != "off" {
driveArgumentString = fmt.Sprintf("%s,detect-zeroes=%s", driveArgumentString, config.DetectZeroes)
}
driveArgs = append(driveArgs, driveArgumentString)
}
} else {
driveArgs = append(driveArgs, fmt.Sprintf("file=%s,if=%s,cache=%s,format=%s", imgPath, config.DiskInterface, config.DiskCache, config.Format))
}
deviceArgs = append(deviceArgs, fmt.Sprintf("%s,netdev=user.0", config.NetDevice))
// Configure virtual CDs
cdPaths := []string{}
// Add the installation CD to the run command
if !config.DiskImage {
isoPath := state.Get("iso_path").(string)
cdPaths = append(cdPaths, isoPath)
}
// Add our custom CD created from cd_files, if it exists
cdFilesPath, ok := state.Get("cd_path").(string)
if ok {
if cdFilesPath != "" {
cdPaths = append(cdPaths, cdFilesPath)
}
}
for i, cdPath := range cdPaths {
if config.CDROMInterface == "" {
driveArgs = append(driveArgs, fmt.Sprintf("file=%s,media=cdrom", cdPath))
} else if config.CDROMInterface == "virtio-scsi" {
driveArgs = append(driveArgs, fmt.Sprintf("file=%s,if=none,index=%d,id=cdrom%d,media=cdrom", cdPath, i, i))
deviceArgs = append(deviceArgs, "virtio-scsi-device", fmt.Sprintf("scsi-cd,drive=cdrom%d", i))
} else {
driveArgs = append(driveArgs, fmt.Sprintf("file=%s,if=%s,index=%d,id=cdrom%d,media=cdrom", cdPath, config.CDROMInterface, i, i))
}
}
return deviceArgs, driveArgs
}
func (s *stepRun) applyUserOverrides(defaultArgs map[string]interface{}, config *Config, state multistep.StateBag) ([]string, error) {
// Done setting up defaults; time to process user args and defaults together
// and generate output args
inArgs := make(map[string][]string)
if len(config.QemuArgs) > 0 {
s.ui.Say("Overriding default Qemu arguments with qemuargs template option...")
commHostPort := 0
if config.CommConfig.Comm.Type != "none" {
if v, ok := state.GetOk("commHostPort"); ok {
commHostPort = v.(int)
}
}
httpIp := state.Get("http_ip").(string)
httpPort := state.Get("http_port").(int)
type qemuArgsTemplateData struct {
HTTPIP string
HTTPPort int
HTTPDir string
HTTPContent map[string]string
OutputDir string
Name string
SSHHostPort int
}
ictx := config.ctx
ictx.Data = qemuArgsTemplateData{
HTTPIP: httpIp,
HTTPPort: httpPort,
HTTPDir: config.HTTPDir,
HTTPContent: config.HTTPContent,
OutputDir: config.OutputDir,
Name: config.VMName,
SSHHostPort: commHostPort,
}
// Interpolate each string in qemuargs
newQemuArgs, err := processArgs(config.QemuArgs, &ictx)
if err != nil {
return nil, err
}
// Qemu supports multiple appearances of the same switch. This means
// each key in the args hash will have an array of string values
for _, qemuArgs := range newQemuArgs {
key := qemuArgs[0]
val := strings.Join(qemuArgs[1:], "")
if _, ok := inArgs[key]; !ok {
inArgs[key] = make([]string, 0)
}
if len(val) > 0 {
inArgs[key] = append(inArgs[key], val)
}
}
}
// get any remaining missing default args from the default settings
for key := range defaultArgs {
if _, ok := inArgs[key]; !ok {
arg := make([]string, 1)
switch defaultArgs[key].(type) {
case string:
arg[0] = defaultArgs[key].(string)
case []string:
arg = defaultArgs[key].([]string)
}
inArgs[key] = arg
}
}
// Check if we are missing the netDevice #6804
if x, ok := inArgs["-device"]; ok {
if !strings.Contains(strings.Join(x, ""), config.NetDevice) {
inArgs["-device"] = append(inArgs["-device"], fmt.Sprintf("%s,netdev=user.0", config.NetDevice))
}
}
// Flatten to array of strings
outArgs := make([]string, 0)
for key, values := range inArgs {
if len(values) > 0 {
for idx := range values {
outArgs = append(outArgs, key, values[idx])
}
} else {
outArgs = append(outArgs, key)
}
}
return outArgs, nil
}
func (s *stepRun) getCommandArgs(config *Config, state multistep.StateBag) ([]string, error) {
defaultArgs := s.getDefaultArgs(config, state)
return s.applyUserOverrides(defaultArgs, config, state)
}
func processArgs(args [][]string, ctx *interpolate.Context) ([][]string, error) {
var err error
if args == nil {
return make([][]string, 0), err
}
newArgs := make([][]string, len(args))
for argsIdx, rowArgs := range args {
parms := make([]string, len(rowArgs))
newArgs[argsIdx] = parms
for i, parm := range rowArgs {
parms[i], err = interpolate.Render(parm, ctx)
if err != nil {
return nil, err
}
}
}
return newArgs, err
}

View File

@ -0,0 +1,54 @@
package qemu
import (
"context"
"fmt"
"net/http"
"github.com/hashicorp/packer-plugin-sdk/multistep"
"github.com/hashicorp/packer-plugin-sdk/net"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
)
// This step set iso_patch to available url
type stepSetISO struct {
ResultKey string
Url []string
}
func (s *stepSetISO) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packersdk.Ui)
iso_path := ""
for _, url := range s.Url {
req, err := http.NewRequest("HEAD", url, nil)
if err != nil {
continue
}
req.Header.Set("User-Agent", "Packer")
httpClient := net.HttpClientWithEnvironmentProxy()
res, err := httpClient.Do(req)
if err == nil && (res.StatusCode >= 200 && res.StatusCode < 300) {
if res.Header.Get("Accept-Ranges") == "bytes" {
iso_path = url
}
}
}
if iso_path == "" {
err := fmt.Errorf("No byte serving support. The HTTP server must support Accept-Ranges=bytes")
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
state.Put(s.ResultKey, iso_path)
return multistep.ActionContinue
}
func (s *stepSetISO) Cleanup(state multistep.StateBag) {}

View File

@ -0,0 +1,94 @@
package qemu
import (
"context"
"errors"
"fmt"
"log"
"time"
"github.com/hashicorp/packer-plugin-sdk/communicator"
"github.com/hashicorp/packer-plugin-sdk/multistep"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
)
// This step shuts down the machine. It first attempts to do so gracefully,
// but ultimately forcefully shuts it down if that fails.
//
// Uses:
// communicator packersdk.Communicator
// config *config
// driver Driver
// ui packersdk.Ui
//
// Produces:
// <nothing>
type stepShutdown struct {
ShutdownCommand string
ShutdownTimeout time.Duration
Comm *communicator.Config
}
func (s *stepShutdown) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
driver := state.Get("driver").(Driver)
ui := state.Get("ui").(packersdk.Ui)
if s.Comm.Type == "none" {
cancelCh := make(chan struct{}, 1)
go func() {
defer close(cancelCh)
<-time.After(s.ShutdownTimeout)
}()
ui.Say("Waiting for shutdown...")
if ok := driver.WaitForShutdown(cancelCh); ok {
log.Println("VM shut down.")
return multistep.ActionContinue
} else {
err := fmt.Errorf("Failed to shutdown")
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
}
if s.ShutdownCommand != "" {
comm := state.Get("communicator").(packersdk.Communicator)
ui.Say("Gracefully halting virtual machine...")
log.Printf("Executing shutdown command: %s", s.ShutdownCommand)
cmd := &packersdk.RemoteCmd{Command: s.ShutdownCommand}
if err := cmd.RunWithUi(ctx, comm, ui); err != nil {
err := fmt.Errorf("Failed to send shutdown command: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
// Start the goroutine that will time out our graceful attempt
cancelCh := make(chan struct{}, 1)
go func() {
defer close(cancelCh)
<-time.After(s.ShutdownTimeout)
}()
log.Printf("Waiting max %s for shutdown to complete", s.ShutdownTimeout)
if ok := driver.WaitForShutdown(cancelCh); !ok {
err := errors.New("Timeout while waiting for machine to shut down.")
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
} else {
ui.Say("Halting the virtual machine...")
if err := driver.Stop(); err != nil {
err := fmt.Errorf("Error stopping VM: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
}
log.Println("VM shut down.")
return multistep.ActionContinue
}
func (s *stepShutdown) Cleanup(state multistep.StateBag) {}

View File

@ -0,0 +1,139 @@
package qemu
import (
"context"
"fmt"
"log"
"net"
"time"
"github.com/hashicorp/packer-plugin-sdk/bootcommand"
"github.com/hashicorp/packer-plugin-sdk/multistep"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
"github.com/hashicorp/packer-plugin-sdk/template/interpolate"
"github.com/mitchellh/go-vnc"
)
const KeyLeftShift uint32 = 0xFFE1
type bootCommandTemplateData struct {
HTTPIP string
HTTPPort int
Name string
}
// This step "types" the boot command into the VM over VNC.
//
// Uses:
// config *config
// http_port int
// ui packersdk.Ui
// vnc_port int
//
// Produces:
// <nothing>
type stepTypeBootCommand struct{}
func (s *stepTypeBootCommand) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*Config)
debug := state.Get("debug").(bool)
httpPort := state.Get("http_port").(int)
ui := state.Get("ui").(packersdk.Ui)
vncPort := state.Get("vnc_port").(int)
vncIP := config.VNCBindAddress
vncPassword := state.Get("vnc_password")
if config.VNCConfig.DisableVNC {
log.Println("Skipping boot command step...")
return multistep.ActionContinue
}
// Wait the for the vm to boot.
if int64(config.BootWait) > 0 {
ui.Say(fmt.Sprintf("Waiting %s for boot...", config.BootWait))
select {
case <-time.After(config.BootWait):
break
case <-ctx.Done():
return multistep.ActionHalt
}
}
var pauseFn multistep.DebugPauseFn
if debug {
pauseFn = state.Get("pauseFn").(multistep.DebugPauseFn)
}
// Connect to VNC
ui.Say(fmt.Sprintf("Connecting to VM via VNC (%s:%d)", vncIP, vncPort))
nc, err := net.Dial("tcp", fmt.Sprintf("%s:%d", vncIP, vncPort))
if err != nil {
err := fmt.Errorf("Error connecting to VNC: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
defer nc.Close()
var auth []vnc.ClientAuth
if vncPassword != nil && len(vncPassword.(string)) > 0 {
auth = []vnc.ClientAuth{&vnc.PasswordAuth{Password: vncPassword.(string)}}
} else {
auth = []vnc.ClientAuth{new(vnc.ClientAuthNone)}
}
c, err := vnc.Client(nc, &vnc.ClientConfig{Auth: auth, Exclusive: false})
if err != nil {
err := fmt.Errorf("Error handshaking with VNC: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
defer c.Close()
log.Printf("Connected to VNC desktop: %s", c.DesktopName)
hostIP := state.Get("http_ip").(string)
configCtx := config.ctx
configCtx.Data = &bootCommandTemplateData{
hostIP,
httpPort,
config.VMName,
}
d := bootcommand.NewVNCDriver(c, config.VNCConfig.BootKeyInterval)
ui.Say("Typing the boot command over VNC...")
command, err := interpolate.Render(config.VNCConfig.FlatBootCommand(), &configCtx)
if err != nil {
err := fmt.Errorf("Error preparing boot command: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
seq, err := bootcommand.GenerateExpressionSequence(command)
if err != nil {
err := fmt.Errorf("Error generating boot command: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
if err := seq.Do(ctx, d); err != nil {
err := fmt.Errorf("Error running boot command: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
if pauseFn != nil {
pauseFn(multistep.DebugLocationAfterRun, fmt.Sprintf("boot_command: %s", command), state)
}
return multistep.ActionContinue
}
func (*stepTypeBootCommand) Cleanup(multistep.StateBag) {}

View File

@ -0,0 +1,136 @@
package qemu
import (
"bufio"
"context"
"fmt"
"log"
"os"
"strconv"
"strings"
"time"
"github.com/hashicorp/packer-plugin-sdk/multistep"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
"github.com/digitalocean/go-qemu/qmp"
)
// This step waits for the guest address to become available in the network
// bridge, then it sets the guestAddress state property.
type stepWaitGuestAddress struct {
CommunicatorType string
NetBridge string
timeout time.Duration
}
func (s *stepWaitGuestAddress) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packersdk.Ui)
if s.CommunicatorType == "none" {
ui.Message("No communicator is configured -- skipping StepWaitGuestAddress")
return multistep.ActionContinue
}
if s.NetBridge == "" {
ui.Message("Not using a NetBridge -- skipping StepWaitGuestAddress")
return multistep.ActionContinue
}
qmpMonitor := state.Get("qmp_monitor").(*qmp.SocketMonitor)
ctx, cancel := context.WithTimeout(ctx, s.timeout)
defer cancel()
ui.Say(fmt.Sprintf("Waiting for the guest address to become available in the %s network bridge...", s.NetBridge))
for {
guestAddress := getGuestAddress(qmpMonitor, s.NetBridge, "user.0")
if guestAddress != "" {
log.Printf("Found guest address %s", guestAddress)
state.Put("guestAddress", guestAddress)
return multistep.ActionContinue
}
select {
case <-time.After(10 * time.Second):
continue
case <-ctx.Done():
return multistep.ActionHalt
}
}
}
func (s *stepWaitGuestAddress) Cleanup(state multistep.StateBag) {
}
func getGuestAddress(qmpMonitor *qmp.SocketMonitor, bridgeName string, deviceName string) string {
devices, err := getNetDevices(qmpMonitor)
if err != nil {
log.Printf("Could not retrieve QEMU QMP network device list: %v", err)
return ""
}
for _, device := range devices {
if device.Name == deviceName {
ipAddress, _ := getDeviceIPAddress(bridgeName, device.MacAddress)
return ipAddress
}
}
log.Printf("QEMU QMP network device %s was not found", deviceName)
return ""
}
func getDeviceIPAddress(device string, macAddress string) (string, error) {
// this parses /proc/net/arp to retrieve the given device IP address.
//
// /proc/net/arp is normally someting alike:
//
// IP address HW type Flags HW address Mask Device
// 192.168.121.111 0x1 0x2 52:54:00:12:34:56 * virbr0
//
const (
IPAddressIndex int = iota
HWTypeIndex
FlagsIndex
HWAddressIndex
MaskIndex
DeviceIndex
)
// see ARP flags at https://github.com/torvalds/linux/blob/v5.4/include/uapi/linux/if_arp.h#L132
const (
AtfCom int = 0x02 // ATF_COM (complete)
)
f, err := os.Open("/proc/net/arp")
if err != nil {
return "", fmt.Errorf("failed to open /proc/net/arp: %w", err)
}
defer f.Close()
s := bufio.NewScanner(f)
s.Scan()
for s.Scan() {
fields := strings.Fields(s.Text())
if device != "" && fields[DeviceIndex] != device {
continue
}
if fields[HWAddressIndex] != macAddress {
continue
}
flags, err := strconv.ParseInt(fields[FlagsIndex], 0, 32)
if err != nil {
return "", fmt.Errorf("failed to parse /proc/net/arp flags field %s: %w", fields[FlagsIndex], err)
}
if int(flags)&AtfCom == AtfCom {
return fields[IPAddressIndex], nil
}
}
return "", fmt.Errorf("could not find %s", macAddress)
}

7
vendor/modules.txt vendored
View File

@ -281,8 +281,7 @@ github.com/digitalocean/go-libvirt
github.com/digitalocean/go-libvirt/internal/constants
github.com/digitalocean/go-libvirt/internal/event
github.com/digitalocean/go-libvirt/internal/go-xdr/xdr2
# github.com/digitalocean/go-qemu v0.0.0-20201211181942-d361e7b4965f
## explicit
# github.com/digitalocean/go-qemu v0.0.0-20210326154740-ac9e0b687001
github.com/digitalocean/go-qemu/qmp
# github.com/digitalocean/godo v1.11.1
## explicit
@ -524,6 +523,9 @@ github.com/hashicorp/packer-plugin-googlecompute/builder/googlecompute
github.com/hashicorp/packer-plugin-googlecompute/builder/googlecompute/version
github.com/hashicorp/packer-plugin-googlecompute/post-processor/googlecompute-export
github.com/hashicorp/packer-plugin-googlecompute/post-processor/googlecompute-import
# github.com/hashicorp/packer-plugin-qemu v0.0.0-20210419131938-9ec808d0c364
## explicit
github.com/hashicorp/packer-plugin-qemu/builder/qemu
# github.com/hashicorp/packer-plugin-sdk v0.2.0
## explicit
github.com/hashicorp/packer-plugin-sdk/acctest
@ -681,7 +683,6 @@ github.com/mitchellh/go-homedir
# github.com/mitchellh/go-testing-interface v1.0.3
github.com/mitchellh/go-testing-interface
# github.com/mitchellh/go-vnc v0.0.0-20150629162542-723ed9867aed
## explicit
github.com/mitchellh/go-vnc
# github.com/mitchellh/go-wordwrap v1.0.0
github.com/mitchellh/go-wordwrap