qemu: add support for using a network bridge

This commit is contained in:
Rui Lopes 2020-05-03 14:47:03 +01:00 committed by Megan Marsh
parent 6ad67f6800
commit 06fad6cc4f
10 changed files with 397 additions and 32 deletions

View File

@ -185,6 +185,16 @@ type Config struct {
// `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
@ -534,7 +544,16 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) {
errs, fmt.Errorf("vnc_port_min must be less than vnc_port_max"))
}
if b.config.VNCUsePassword && b.config.QMPSocketPath == "" {
if b.config.NetBridge != "" && runtime.GOOS != "linux" {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("net_bridge is only supported in Linux based OSes"))
}
if b.config.NetBridge != "" || b.config.VNCUsePassword {
b.config.QMPEnable = true
}
if b.config.QMPEnable && b.config.QMPSocketPath == "" {
socketName := fmt.Sprintf("%s.monitor", b.config.VMName)
b.config.QMPSocketPath = filepath.Join(b.config.OutputDir, socketName)
}
@ -603,7 +622,7 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
},
)
if b.config.Comm.Type != "none" {
if b.config.Comm.Type != "none" && b.config.NetBridge == "" {
steps = append(steps,
new(stepForwardSSH),
)
@ -613,12 +632,17 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
new(stepConfigureVNC),
steprun,
&stepConfigureQMP{
VNCUsePassword: b.config.VNCUsePassword,
QMPSocketPath: b.config.QMPSocketPath,
QMPSocketPath: b.config.QMPSocketPath,
},
&stepTypeBootCommand{},
)
if b.config.Comm.Type != "none" && b.config.NetBridge != "" {
steps = append(steps,
new(stepWaitGuestAddress),
)
}
if b.config.Comm.Type != "none" {
steps = append(steps,
&communicator.StepConnect{

View File

@ -96,6 +96,7 @@ type FlatConfig struct {
MachineType *string `mapstructure:"machine_type" required:"false" cty:"machine_type"`
MemorySize *int `mapstructure:"memory" required:"false" cty:"memory"`
NetDevice *string `mapstructure:"net_device" required:"false" cty:"net_device"`
NetBridge *string `mapstructure:"net_bridge" required:"false" cty:"net_bridge"`
OutputDir *string `mapstructure:"output_directory" required:"false" cty:"output_directory"`
QemuArgs [][]string `mapstructure:"qemuargs" required:"false" cty:"qemuargs"`
QemuBinary *string `mapstructure:"qemu_binary" required:"false" cty:"qemu_binary"`
@ -212,6 +213,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
"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_binary": &hcldec.AttrSpec{Name: "qemu_binary", Type: cty.String, Required: false},

135
builder/qemu/qmp.go Normal file
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

@ -13,11 +13,18 @@ func commHost(host string) func(multistep.StateBag) (string, error) {
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) {
sshHostPort := state.Get("sshHostPort").(int)
sshHostPort, ok := state.Get("sshHostPort").(int)
if !ok {
sshHostPort = 22
}
return int(sshHostPort), nil
}

View File

@ -20,15 +20,15 @@ import (
//
// Produces:
type stepConfigureQMP struct {
monitor *qmp.SocketMonitor
VNCUsePassword bool
QMPSocketPath string
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").(packer.Ui)
if !s.VNCUsePassword {
if !config.QMPEnable {
return multistep.ActionContinue
}
@ -46,12 +46,10 @@ func (s *stepConfigureQMP) Run(ctx context.Context, state multistep.StateBag) mu
ui.Error(err.Error())
return multistep.ActionHalt
}
QMPMonitor := s.monitor
vncPassword := state.Get("vnc_password")
// Connect to QMP
// function automatically calls capabilities so is immediately ready for commands
err = QMPMonitor.Connect()
err = s.monitor.Connect()
if err != nil {
err := fmt.Errorf("Error connecting to QMP socket: %s", err)
state.Put("error", err)
@ -60,21 +58,22 @@ func (s *stepConfigureQMP) Run(ctx context.Context, state multistep.StateBag) mu
}
log.Printf("QMP socket open SUCCESS")
cmd = []byte(fmt.Sprintf("{ \"execute\": \"change-vnc-password\", \"arguments\": { \"password\": \"%s\" } }",
vncPassword))
result, err = QMPMonitor.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
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)
}
log.Printf("QMP Command: %s\nResult: %s", cmd, result)
// Put QMP monitor in statebag in case there is a use in a following step
// Uncomment for future case as it is unused for now
//state.Put("qmp_monitor", QMPMonitor)
// make the qmp_monitor available to other steps.
state.Put("qmp_monitor", s.monitor)
return multistep.ActionContinue
}

View File

@ -2,8 +2,11 @@ package qemu
import (
"context"
"fmt"
"net"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
)
// Step to discover the http ip
@ -12,7 +15,52 @@ import (
type stepHTTPIPDiscover struct{}
func (s *stepHTTPIPDiscover) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
state.Put("http_ip", "10.0.2.2")
config := state.Get("config").(*Config)
ui := state.Get("ui").(packer.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
}

View File

@ -1,14 +1,22 @@
package qemu
import (
"bytes"
"context"
"testing"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
)
func TestStepHTTPIPDiscover_Run(t *testing.T) {
state := new(multistep.BasicStateBag)
state.Put("ui", &packer.BasicUi{
Reader: new(bytes.Buffer),
Writer: new(bytes.Buffer),
})
config := &Config{}
state.Put("config", config)
step := new(stepHTTPIPDiscover)
hostIp := "10.0.2.2"

View File

@ -8,6 +8,7 @@ import (
"strings"
"github.com/hashicorp/go-version"
"github.com/hashicorp/packer/common"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
"github.com/hashicorp/packer/template/interpolate"
@ -79,16 +80,24 @@ func getCommandArgs(bootDrive string, state multistep.StateBag) ([]string, error
vnc = fmt.Sprintf("%s:%d", vncIP, vncPort-5900)
} else {
vnc = fmt.Sprintf("%s:%d,password", vncIP, vncPort-5900)
}
if config.QMPEnable {
defaultArgs["-qmp"] = fmt.Sprintf("unix:%s,server,nowait", config.QMPSocketPath)
}
defaultArgs["-name"] = vmName
defaultArgs["-machine"] = fmt.Sprintf("type=%s", config.MachineType)
if config.Comm.Type != "none" {
sshHostPort = state.Get("sshHostPort").(int)
defaultArgs["-netdev"] = fmt.Sprintf("user,id=user.0,hostfwd=tcp::%v-:%d", sshHostPort, config.Comm.Port())
if config.NetBridge == "" {
if config.Comm.Type != "none" {
sshHostPort = state.Get("sshHostPort").(int)
defaultArgs["-netdev"] = fmt.Sprintf("user,id=user.0,hostfwd=tcp::%v-:%d", sshHostPort, config.Comm.Port())
} else {
defaultArgs["-netdev"] = fmt.Sprintf("user,id=user.0")
}
} else {
defaultArgs["-netdev"] = fmt.Sprintf("user,id=user.0")
defaultArgs["-netdev"] = fmt.Sprintf("bridge,id=user.0,br=%s", config.NetBridge)
}
rawVersion, err := driver.Version()
@ -215,11 +224,12 @@ func getCommandArgs(bootDrive string, state multistep.StateBag) ([]string, error
if len(config.QemuArgs) > 0 {
ui.Say("Overriding defaults Qemu arguments with QemuArgs...")
httpIp := common.GetHTTPIP()
httpPort := state.Get("http_port").(int)
ictx := config.ctx
if config.Comm.Type != "none" {
ictx.Data = qemuArgsTemplateData{
"10.0.2.2",
httpIp,
httpPort,
config.HTTPDir,
config.OutputDir,
@ -228,7 +238,7 @@ func getCommandArgs(bootDrive string, state multistep.StateBag) ([]string, error
}
} else {
ictx.Data = qemuArgsTemplateData{
HTTPIP: "10.0.2.2",
HTTPIP: httpIp,
HTTPPort: httpPort,
HTTPDir: config.HTTPDir,
OutputDir: config.OutputDir,

View File

@ -0,0 +1,122 @@
package qemu
import (
"bufio"
"context"
"fmt"
"log"
"os"
"strconv"
"strings"
"time"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/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{}
func (s *stepWaitGuestAddress) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*Config)
ui := state.Get("ui").(packer.Ui)
qmpMonitor := state.Get("qmp_monitor").(*qmp.SocketMonitor)
ui.Say(fmt.Sprintf("Waiting for the guest address to become available in the %s network bridge...", config.NetBridge))
for {
guestAddress := getGuestAddress(qmpMonitor, config.NetBridge, "user.0")
if guestAddress != "" {
log.Printf("Found guest address %s", guestAddress)
state.Put("guestAddress", guestAddress)
break
}
select {
case <-time.After(10 * time.Second):
continue
case <-ctx.Done():
return multistep.ActionHalt
}
}
return multistep.ActionContinue
}
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)
}

View File

@ -108,6 +108,16 @@
`vmxnet3`, `i82558a` or `i82558b`. The Qemu builder uses `virtio-net` by
default.
- `net_bridge` (string) - 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.
- `output_directory` (string) - 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