1126 lines
28 KiB
Go
1126 lines
28 KiB
Go
package libvirt
|
|
|
|
import (
|
|
"context"
|
|
"encoding/xml"
|
|
"fmt"
|
|
"log"
|
|
"net"
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/davecgh/go-spew/spew"
|
|
libvirt "github.com/digitalocean/go-libvirt"
|
|
"github.com/dmacvicar/terraform-provider-libvirt/libvirt/helper/suppress"
|
|
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
|
|
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
|
|
"libvirt.org/go/libvirtxml"
|
|
)
|
|
|
|
type pendingMapping struct {
|
|
mac string
|
|
hostname string
|
|
networkName string
|
|
}
|
|
|
|
func init() {
|
|
spew.Config.Indent = "\t"
|
|
}
|
|
|
|
func resourceLibvirtDomain() *schema.Resource {
|
|
return &schema.Resource{
|
|
CreateContext: resourceLibvirtDomainCreate,
|
|
ReadContext: resourceLibvirtDomainRead,
|
|
DeleteContext: resourceLibvirtDomainDelete,
|
|
UpdateContext: resourceLibvirtDomainUpdate,
|
|
Importer: &schema.ResourceImporter{
|
|
StateContext: schema.ImportStatePassthroughContext,
|
|
},
|
|
Timeouts: &schema.ResourceTimeout{
|
|
//nolint:mnd
|
|
Create: schema.DefaultTimeout(5 * time.Minute),
|
|
},
|
|
Schema: map[string]*schema.Schema{
|
|
"name": {
|
|
Type: schema.TypeString,
|
|
Required: true,
|
|
ForceNew: true,
|
|
},
|
|
"description": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
ForceNew: true,
|
|
},
|
|
"metadata": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
ForceNew: false,
|
|
},
|
|
"vcpu": {
|
|
Type: schema.TypeInt,
|
|
Optional: true,
|
|
Default: 1,
|
|
ForceNew: true,
|
|
},
|
|
"memory": {
|
|
Type: schema.TypeInt,
|
|
Optional: true,
|
|
Default: defaultDomainMemoryMiB,
|
|
ForceNew: true,
|
|
},
|
|
"firmware": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
ForceNew: true,
|
|
},
|
|
"type": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
ForceNew: true,
|
|
Default: "kvm",
|
|
},
|
|
"nvram": {
|
|
Type: schema.TypeList,
|
|
Optional: true,
|
|
ForceNew: true,
|
|
Computed: true,
|
|
MaxItems: 1,
|
|
Elem: &schema.Resource{
|
|
Schema: map[string]*schema.Schema{
|
|
"file": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
ForceNew: true,
|
|
Computed: true,
|
|
},
|
|
"template": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
ForceNew: true,
|
|
Computed: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"running": {
|
|
Type: schema.TypeBool,
|
|
Optional: true,
|
|
Default: true,
|
|
ForceNew: false,
|
|
Required: false,
|
|
},
|
|
"cloudinit": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
ForceNew: false,
|
|
},
|
|
"coreos_ignition": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
ForceNew: true,
|
|
Default: "",
|
|
},
|
|
"fw_cfg_name": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
ForceNew: true,
|
|
Default: "opt/com.coreos/config",
|
|
},
|
|
"filesystem": {
|
|
Type: schema.TypeList,
|
|
Optional: true,
|
|
ForceNew: true,
|
|
Elem: &schema.Resource{
|
|
Schema: map[string]*schema.Schema{
|
|
"accessmode": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
Default: "mapped",
|
|
},
|
|
"source": {
|
|
Type: schema.TypeString,
|
|
Required: true,
|
|
},
|
|
"target": {
|
|
Type: schema.TypeString,
|
|
Required: true,
|
|
},
|
|
"readonly": {
|
|
Type: schema.TypeBool,
|
|
Optional: true,
|
|
Default: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"disk": {
|
|
Type: schema.TypeList,
|
|
Optional: true,
|
|
ForceNew: true,
|
|
Elem: &schema.Resource{
|
|
Schema: map[string]*schema.Schema{
|
|
"volume_id": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
},
|
|
"url": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
},
|
|
"file": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
},
|
|
"scsi": {
|
|
Type: schema.TypeBool,
|
|
Optional: true,
|
|
ForceNew: true,
|
|
Default: false,
|
|
},
|
|
"wwn": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
Computed: true,
|
|
},
|
|
"block_device": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"network_interface": {
|
|
Type: schema.TypeList,
|
|
Optional: true,
|
|
Elem: &schema.Resource{
|
|
Schema: map[string]*schema.Schema{
|
|
"network_id": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
Computed: true,
|
|
},
|
|
"network_name": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
Computed: true,
|
|
},
|
|
"bridge": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
},
|
|
"vepa": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
},
|
|
"macvtap": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
},
|
|
"passthrough": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
},
|
|
"hostname": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
Computed: true,
|
|
},
|
|
"mac": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
Computed: true,
|
|
DiffSuppressFunc: suppress.CaseDifference,
|
|
},
|
|
"wait_for_lease": {
|
|
Type: schema.TypeBool,
|
|
Optional: true,
|
|
},
|
|
"addresses": {
|
|
Type: schema.TypeList,
|
|
Optional: true,
|
|
Computed: true,
|
|
Elem: &schema.Schema{
|
|
Type: schema.TypeString,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"graphics": {
|
|
ForceNew: true,
|
|
Type: schema.TypeList,
|
|
Optional: true,
|
|
MaxItems: 1,
|
|
Elem: &schema.Resource{
|
|
Schema: map[string]*schema.Schema{
|
|
"type": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
Default: "spice",
|
|
ForceNew: true,
|
|
},
|
|
"autoport": {
|
|
Type: schema.TypeBool,
|
|
Optional: true,
|
|
Default: true,
|
|
ForceNew: true,
|
|
},
|
|
"listen_type": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
Default: "none",
|
|
ForceNew: true,
|
|
},
|
|
"listen_address": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
Default: "127.0.0.1",
|
|
ForceNew: true,
|
|
},
|
|
"websocket": {
|
|
Type: schema.TypeInt,
|
|
Optional: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"video": {
|
|
Type: schema.TypeList,
|
|
Optional: true,
|
|
MaxItems: 1,
|
|
Elem: &schema.Resource{
|
|
Schema: map[string]*schema.Schema{
|
|
"type": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
Default: "cirrus",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"console": {
|
|
Type: schema.TypeList,
|
|
Optional: true,
|
|
Elem: &schema.Resource{
|
|
Schema: map[string]*schema.Schema{
|
|
"type": {
|
|
Type: schema.TypeString,
|
|
Required: true,
|
|
ForceNew: true,
|
|
},
|
|
"source_path": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
ForceNew: true,
|
|
},
|
|
"source_host": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
ForceNew: true,
|
|
Default: "127.0.0.1",
|
|
},
|
|
"source_service": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
ForceNew: true,
|
|
Default: "0",
|
|
},
|
|
"target_port": {
|
|
Type: schema.TypeString,
|
|
Required: true,
|
|
ForceNew: true,
|
|
},
|
|
"target_type": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
ForceNew: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"cpu": {
|
|
Type: schema.TypeList,
|
|
MaxItems: 1,
|
|
Optional: true,
|
|
Computed: true,
|
|
ForceNew: true,
|
|
Elem: &schema.Resource{
|
|
Schema: map[string]*schema.Schema{
|
|
"mode": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
Computed: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"autostart": {
|
|
Type: schema.TypeBool,
|
|
Optional: true,
|
|
Computed: true,
|
|
},
|
|
"machine": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
Computed: true,
|
|
},
|
|
"arch": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
Computed: true,
|
|
},
|
|
"boot_device": {
|
|
Type: schema.TypeList,
|
|
Optional: true,
|
|
Required: false,
|
|
Elem: &schema.Resource{
|
|
Schema: map[string]*schema.Schema{
|
|
"dev": {
|
|
Type: schema.TypeList,
|
|
Optional: true,
|
|
Required: false,
|
|
Elem: &schema.Schema{
|
|
Type: schema.TypeString,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"emulator": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
Computed: true,
|
|
},
|
|
"kernel": {
|
|
Type: schema.TypeString,
|
|
Required: false,
|
|
Optional: true,
|
|
ForceNew: false,
|
|
},
|
|
"initrd": {
|
|
Type: schema.TypeString,
|
|
Required: false,
|
|
Optional: true,
|
|
ForceNew: false,
|
|
},
|
|
"cmdline": {
|
|
Type: schema.TypeList,
|
|
Optional: true,
|
|
Required: false,
|
|
ForceNew: true,
|
|
Elem: &schema.Schema{
|
|
Type: schema.TypeMap,
|
|
},
|
|
},
|
|
"qemu_agent": {
|
|
Type: schema.TypeBool,
|
|
Optional: true,
|
|
Required: false,
|
|
Default: false,
|
|
ForceNew: false,
|
|
},
|
|
"tpm": {
|
|
Type: schema.TypeList,
|
|
Optional: true,
|
|
// Error defining libvirt domain: unsupported configuration: only a single TPM non-proxy device is supported
|
|
MaxItems: 1,
|
|
Elem: &schema.Resource{
|
|
Schema: map[string]*schema.Schema{
|
|
"model": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
ForceNew: true,
|
|
},
|
|
"backend_type": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
ForceNew: true,
|
|
Default: "emulator",
|
|
},
|
|
"backend_device_path": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
ForceNew: true,
|
|
},
|
|
"backend_encryption_secret": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
ForceNew: true,
|
|
},
|
|
"backend_version": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
ForceNew: true,
|
|
},
|
|
"backend_persistent_state": {
|
|
Type: schema.TypeBool,
|
|
Optional: true,
|
|
ForceNew: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"xml": {
|
|
Type: schema.TypeList,
|
|
Optional: true,
|
|
MaxItems: 1,
|
|
Elem: &schema.Resource{
|
|
Schema: map[string]*schema.Schema{
|
|
"xslt": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
ForceNew: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func resourceLibvirtDomainCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
|
|
log.Printf("[DEBUG] Create resource libvirt_domain")
|
|
|
|
virConn := meta.(*Client).libvirt
|
|
|
|
domainDef, err := newDomainDefForConnection(virConn, d)
|
|
if err != nil {
|
|
return diag.FromErr(err)
|
|
}
|
|
|
|
if name, ok := d.GetOk("name"); ok {
|
|
domainDef.Name = name.(string)
|
|
}
|
|
|
|
if cpuMode, ok := d.GetOk("cpu.0.mode"); ok {
|
|
domainDef.CPU = &libvirtxml.DomainCPU{
|
|
Mode: cpuMode.(string),
|
|
}
|
|
}
|
|
|
|
domainDef.Memory = &libvirtxml.DomainMemory{
|
|
Value: uint(d.Get("memory").(int)),
|
|
Unit: "MiB",
|
|
}
|
|
domainDef.VCPU = &libvirtxml.DomainVCPU{
|
|
Value: uint(d.Get("vcpu").(int)),
|
|
}
|
|
domainDef.Description = d.Get("description").(string)
|
|
|
|
domainDef.OS.Kernel = d.Get("kernel").(string)
|
|
domainDef.OS.Initrd = d.Get("initrd").(string)
|
|
domainDef.OS.Type.Arch = d.Get("arch").(string)
|
|
|
|
domainDef.Devices.Emulator = d.Get("emulator").(string)
|
|
|
|
if v := os.Getenv("TERRAFORM_LIBVIRT_TEST_DOMAIN_TYPE"); v != "" {
|
|
domainDef.Type = v
|
|
} else {
|
|
domainDef.Type = d.Get("type").(string)
|
|
}
|
|
|
|
arch, err := getHostArchitecture(virConn)
|
|
if err != nil {
|
|
return diag.Errorf("error retrieving host architecture: %s", err)
|
|
}
|
|
|
|
if err := setGraphics(d, &domainDef, arch); err != nil {
|
|
return diag.FromErr(err)
|
|
}
|
|
|
|
setVideo(d, &domainDef)
|
|
|
|
if err := setConsoles(d, &domainDef); err != nil {
|
|
return diag.FromErr(err)
|
|
}
|
|
|
|
setCmdlineArgs(d, &domainDef)
|
|
setFirmware(d, &domainDef)
|
|
setBootDevices(d, &domainDef)
|
|
setTPMs(d, &domainDef)
|
|
|
|
if err := setCoreOSIgnition(d, &domainDef, arch); err != nil {
|
|
return diag.FromErr(err)
|
|
}
|
|
|
|
if err := setDisks(d, &domainDef, virConn); err != nil {
|
|
return diag.FromErr(err)
|
|
}
|
|
|
|
if err := setFilesystems(d, &domainDef); err != nil {
|
|
return diag.FromErr(err)
|
|
}
|
|
|
|
if err := setCloudinit(d, &domainDef, virConn); err != nil {
|
|
return diag.FromErr(err)
|
|
}
|
|
|
|
var waitForLeases []*libvirtxml.DomainInterface
|
|
partialNetIfaces := make(map[string]*pendingMapping, d.Get("network_interface.#").(int))
|
|
|
|
if err := setNetworkInterfaces(d, &domainDef, virConn, partialNetIfaces, &waitForLeases); err != nil {
|
|
return diag.FromErr(err)
|
|
}
|
|
|
|
connectURI, err := virConn.ConnectGetUri()
|
|
if err != nil {
|
|
return diag.Errorf("error retrieving libvirt connection URI: %s", err)
|
|
}
|
|
log.Printf("[INFO] Creating libvirt domain at %s", connectURI)
|
|
|
|
data, err := xmlMarshallIndented(domainDef)
|
|
if err != nil {
|
|
return diag.Errorf("error serializing libvirt domain: %s", err)
|
|
}
|
|
log.Printf("[DEBUG] Generated XML for libvirt domain:\n%s", data)
|
|
|
|
data, err = transformResourceXML(data, d)
|
|
if err != nil {
|
|
return diag.Errorf("error applying XSLT stylesheet: %s", err)
|
|
}
|
|
|
|
domain, err := virConn.DomainDefineXML(data)
|
|
if err != nil {
|
|
return diag.Errorf("error defining libvirt domain: %s", err)
|
|
}
|
|
|
|
if autostart, ok := d.GetOk("autostart"); ok {
|
|
var autostartInt int32
|
|
if autostart.(bool) {
|
|
autostartInt = 1
|
|
}
|
|
err = virConn.DomainSetAutostart(domain, autostartInt)
|
|
if err != nil {
|
|
return diag.Errorf("error setting autostart for domain: %s", err)
|
|
}
|
|
}
|
|
|
|
err = virConn.DomainCreate(domain)
|
|
if err != nil {
|
|
return diag.Errorf("error creating libvirt domain: %s", err)
|
|
}
|
|
|
|
id := uuidString(domain.UUID)
|
|
d.SetId(id)
|
|
log.Printf("[INFO] Domain ID: %s", d.Id())
|
|
|
|
if len(waitForLeases) > 0 {
|
|
if err := waitForStateDomainLeaseDone(ctx, virConn, domain, waitForLeases, d); err != nil {
|
|
return diag.FromErr(err)
|
|
}
|
|
}
|
|
|
|
// We save runnig state to not mix what we have and what we want
|
|
requiredStatus := d.Get("running")
|
|
|
|
if diag := resourceLibvirtDomainRead(ctx, d, meta); diag.HasError() {
|
|
return diag
|
|
}
|
|
|
|
d.Set("running", requiredStatus)
|
|
|
|
// we must read devices again in order to set some missing ip/MAC/host mappings
|
|
for i := 0; i < d.Get("network_interface.#").(int); i++ {
|
|
prefix := fmt.Sprintf("network_interface.%d", i)
|
|
mac := strings.ToUpper(d.Get(prefix + ".mac").(string))
|
|
log.Printf("[DEBUG] Reading network_interface.%d with MAC: %s\n", i, mac)
|
|
|
|
// if we were waiting for an IP address for this MAC, go ahead.
|
|
if pending, ok := partialNetIfaces[mac]; ok {
|
|
// we should have the address now
|
|
addressesI, ok := d.GetOk(prefix + ".addresses")
|
|
if !ok {
|
|
log.Printf("Did not obtain the IP address for MAC=%s", mac)
|
|
continue
|
|
}
|
|
|
|
network, err := virConn.NetworkLookupByName(pending.networkName)
|
|
if err != nil {
|
|
log.Printf("Can't retrieve network '%s'", pending.networkName)
|
|
continue
|
|
}
|
|
|
|
for _, addressI := range addressesI.([]interface{}) {
|
|
address := addressI.(string)
|
|
log.Printf("[INFO] Finally adding IP/MAC/host=%s/%s/%s", address, mac, pending.hostname)
|
|
|
|
err = updateOrAddHost(virConn, network, address, mac, pending.hostname)
|
|
if err != nil {
|
|
log.Printf("Could not add IP/MAC/host=%s/%s/%s: %s", address, mac, pending.hostname, err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if err := destroyDomainByUserRequest(virConn, d, domain); err != nil {
|
|
return diag.FromErr(err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func resourceLibvirtDomainUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
|
|
log.Printf("[DEBUG] Update resource libvirt_domain")
|
|
|
|
virConn := meta.(*Client).libvirt
|
|
|
|
uuid := parseUUID(d.Id())
|
|
|
|
domain, err := virConn.DomainLookupByUUID(uuid)
|
|
if err != nil {
|
|
return diag.Errorf("error retrieving libvirt domain by update: %s", err)
|
|
}
|
|
|
|
domainRunningNow, err := domainIsRunning(virConn, domain)
|
|
if err != nil {
|
|
return diag.FromErr(err)
|
|
}
|
|
|
|
if !domainRunningNow {
|
|
err = virConn.DomainCreate(domain)
|
|
if err != nil {
|
|
return diag.Errorf("error creating libvirt domain: %s", err)
|
|
}
|
|
}
|
|
|
|
if d.HasChange("cloudinit") {
|
|
cloudinitID, err := getCloudInitVolumeKeyFromTerraformID(d.Get("cloudinit").(string))
|
|
if err != nil {
|
|
return diag.FromErr(err)
|
|
}
|
|
|
|
disk, err := newDiskForCloudInit(virConn, cloudinitID)
|
|
if err != nil {
|
|
return diag.FromErr(err)
|
|
}
|
|
|
|
data, err := xml.Marshal(disk)
|
|
if err != nil {
|
|
return diag.Errorf("error serializing cloudinit disk: %s", err)
|
|
}
|
|
|
|
err = virConn.DomainUpdateDeviceFlags(domain,
|
|
string(data),
|
|
libvirt.DomainDeviceModifyConfig|libvirt.DomainDeviceModifyCurrent|libvirt.DomainDeviceModifyLive)
|
|
if err != nil {
|
|
return diag.Errorf("error while changing the cloudinit volume: %s", err)
|
|
}
|
|
}
|
|
|
|
if d.HasChange("autostart") {
|
|
var autoStart int32
|
|
if d.Get("autostart").(bool) {
|
|
autoStart = 1
|
|
}
|
|
|
|
err = virConn.DomainSetAutostart(domain, autoStart)
|
|
if err != nil {
|
|
return diag.Errorf("error setting autostart for domain: %s", err)
|
|
}
|
|
}
|
|
|
|
netIfacesCount := d.Get("network_interface.#").(int)
|
|
|
|
for i := 0; i < netIfacesCount; i++ {
|
|
prefix := fmt.Sprintf("network_interface.%d", i)
|
|
if d.HasChange(prefix+".hostname") || d.HasChange(prefix+".addresses") || d.HasChange(prefix+".mac") {
|
|
networkUUID, ok := d.GetOk(prefix + ".network_id")
|
|
log.Printf("[INFO] NetworkUUID: %s\n", networkUUID)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
uuid := parseUUID(networkUUID.(string))
|
|
|
|
network, err := virConn.NetworkLookupByUUID(uuid)
|
|
if err != nil {
|
|
return diag.Errorf("can't retrieve network ID %s", networkUUID)
|
|
}
|
|
|
|
hostname := d.Get(prefix + ".hostname").(string)
|
|
mac := d.Get(prefix + ".mac").(string)
|
|
addresses := d.Get(prefix + ".addresses")
|
|
for _, addressI := range addresses.([]interface{}) {
|
|
address := addressI.(string)
|
|
|
|
ip := net.ParseIP(address)
|
|
if ip == nil {
|
|
return diag.Errorf("could not parse addresses '%s'", address)
|
|
}
|
|
|
|
log.Printf("[INFO] Updating IP/MAC/host=%s/%s/%s in '%s' network", ip.String(), mac, hostname, network.Name)
|
|
|
|
if err := updateOrAddHost(virConn, network, ip.String(), mac, hostname); err != nil {
|
|
return diag.FromErr(err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func resourceLibvirtDomainRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
|
|
log.Printf("[DEBUG] Read resource libvirt_domain")
|
|
|
|
virConn := meta.(*Client).libvirt
|
|
|
|
uuid := parseUUID(d.Id())
|
|
|
|
domain, err := virConn.DomainLookupByUUID(uuid)
|
|
if err != nil {
|
|
if isError(err, libvirt.ErrNoDomain) {
|
|
d.SetId("")
|
|
return nil
|
|
}
|
|
return diag.Errorf("error retrieving libvirt domain: %s", err)
|
|
}
|
|
|
|
xmlDesc, err := virConn.DomainGetXMLDesc(domain, 0)
|
|
if err != nil {
|
|
return diag.Errorf("error retrieving libvirt domain XML description: %s", err)
|
|
}
|
|
|
|
log.Printf("[DEBUG] read: obtained XML desc for domain:\n%s", xmlDesc)
|
|
|
|
domainDef, err := newDomainDefForConnection(virConn, d)
|
|
if err != nil {
|
|
return diag.FromErr(err)
|
|
}
|
|
|
|
err = xml.Unmarshal([]byte(xmlDesc), &domainDef)
|
|
if err != nil {
|
|
return diag.Errorf("error reading libvirt domain XML description: %s", err)
|
|
}
|
|
|
|
autostart, err := virConn.DomainGetAutostart(domain)
|
|
if err != nil {
|
|
return diag.Errorf("error reading domain autostart setting: %s", err)
|
|
}
|
|
_ = d.Set("autostart", autostart > 0)
|
|
|
|
domainRunningNow, err := domainIsRunning(virConn, domain)
|
|
if err != nil {
|
|
return diag.Errorf("error reading domain running state : %s", err)
|
|
}
|
|
|
|
d.Set("name", domainDef.Name)
|
|
d.Set("description", domainDef.Description)
|
|
d.Set("vcpu", domainDef.VCPU.Value)
|
|
|
|
switch domainDef.Memory.Unit {
|
|
case "KiB":
|
|
d.Set("memory", domainDef.Memory.Value/1024)
|
|
case "MiB":
|
|
d.Set("memory", domainDef.Memory.Value)
|
|
default:
|
|
return diag.Errorf("invalid memory unit : %s", domainDef.Memory.Unit)
|
|
}
|
|
|
|
if domainDef.OS.Loader != nil {
|
|
d.Set("firmware", domainDef.OS.Loader.Path)
|
|
}
|
|
|
|
if domainDef.OS.NVRam != nil {
|
|
nvram := map[string]interface{}{}
|
|
if domainDef.OS.NVRam.NVRam != "" {
|
|
nvram["file"] = domainDef.OS.NVRam.NVRam
|
|
}
|
|
|
|
if domainDef.OS.NVRam.Template != "" {
|
|
nvram["template"] = domainDef.OS.NVRam.Template
|
|
}
|
|
|
|
d.Set("nvram", []map[string]interface{}{nvram})
|
|
}
|
|
|
|
if domainDef.CPU != nil {
|
|
cpu := make(map[string]interface{})
|
|
var cpus []map[string]interface{}
|
|
if domainDef.CPU.Mode != "" {
|
|
cpu["mode"] = domainDef.CPU.Mode
|
|
}
|
|
if len(cpu) > 0 {
|
|
cpus = append(cpus, cpu)
|
|
d.Set("cpu", cpus)
|
|
}
|
|
}
|
|
|
|
d.Set("arch", domainDef.OS.Type.Arch)
|
|
d.Set("running", domainRunningNow)
|
|
|
|
cmdLines := splitKernelCmdLine(domainDef.OS.Cmdline)
|
|
|
|
d.Set("cmdline", cmdLines)
|
|
d.Set("kernel", domainDef.OS.Kernel)
|
|
d.Set("initrd", domainDef.OS.Initrd)
|
|
|
|
caps, err := getHostCapabilities(virConn)
|
|
if err != nil {
|
|
return diag.FromErr(err)
|
|
}
|
|
machine, err := getOriginalMachineName(caps, domainDef.OS.Type.Arch, domainDef.OS.Type.Type,
|
|
domainDef.OS.Type.Machine)
|
|
if err != nil {
|
|
return diag.FromErr(err)
|
|
}
|
|
d.Set("machine", machine)
|
|
|
|
// Emulator is the same as the default don't set it in domainDef
|
|
// or it will show as changed
|
|
d.Set("emulator", domainDef.Devices.Emulator)
|
|
var (
|
|
disks []map[string]interface{}
|
|
disk map[string]interface{}
|
|
)
|
|
for _, diskDef := range domainDef.Devices.Disks {
|
|
// network drives do not have a volume associated
|
|
if diskDef.Source.Network != nil {
|
|
if len(diskDef.Source.Network.Hosts) < 1 {
|
|
return diag.Errorf("network disk does not contain any hosts")
|
|
}
|
|
url, err := url.Parse(fmt.Sprintf("%s://%s:%s%s",
|
|
diskDef.Source.Network.Protocol,
|
|
diskDef.Source.Network.Hosts[0].Name,
|
|
diskDef.Source.Network.Hosts[0].Port,
|
|
diskDef.Source.Network.Name))
|
|
if err != nil {
|
|
return diag.FromErr(err)
|
|
}
|
|
disk = map[string]interface{}{
|
|
"url": url.String(),
|
|
}
|
|
} else if diskDef.Device == "cdrom" {
|
|
// HACK we marked the disk as belonging to the cloudinit
|
|
// resource so we can ignore it
|
|
if diskDef.Serial == "cloudinit" {
|
|
continue
|
|
}
|
|
|
|
disk = map[string]interface{}{
|
|
"file": diskDef.Source.File.File,
|
|
}
|
|
} else if diskDef.Source.Block != nil {
|
|
disk = map[string]interface{}{
|
|
"block_device": diskDef.Source.Block.Dev,
|
|
}
|
|
} else if diskDef.Source.File != nil {
|
|
// LEGACY way of handling volumes using "file", which we replaced
|
|
// by the diskdef.Source.Volume once we realized it existed.
|
|
// This code will be removed in future versions of the provider.
|
|
virVol, err := virConn.StorageVolLookupByPath(diskDef.Source.File.File)
|
|
if err != nil {
|
|
return diag.Errorf("error retrieving volume for disk: %s", err)
|
|
}
|
|
|
|
disk = map[string]interface{}{
|
|
"volume_id": virVol.Key,
|
|
}
|
|
} else {
|
|
pool, err := virConn.StoragePoolLookupByName(diskDef.Source.Volume.Pool)
|
|
if err != nil {
|
|
return diag.Errorf("error retrieving pool for disk: %s", err)
|
|
}
|
|
|
|
virVol, err := virConn.StorageVolLookupByName(pool, diskDef.Source.Volume.Volume)
|
|
if err != nil {
|
|
return diag.Errorf("error retrieving volume for disk: %s", err)
|
|
}
|
|
|
|
disk = map[string]interface{}{
|
|
"volume_id": virVol.Key,
|
|
}
|
|
}
|
|
|
|
if diskDef.Target != nil && diskDef.Target.Bus == "scsi" {
|
|
disk["scsi"] = true
|
|
disk["wwn"] = diskDef.WWN
|
|
} else {
|
|
disk["scsi"] = false
|
|
}
|
|
|
|
disks = append(disks, disk)
|
|
}
|
|
|
|
if len(disks) > 0 {
|
|
d.Set("disk", disks)
|
|
}
|
|
|
|
var filesystems []map[string]interface{}
|
|
for _, fsDef := range domainDef.Devices.Filesystems {
|
|
fs := map[string]interface{}{
|
|
"accessmode": fsDef.AccessMode,
|
|
"source": fsDef.Source.Mount.Dir,
|
|
"target": fsDef.Target.Dir,
|
|
"readonly": fsDef.ReadOnly,
|
|
}
|
|
filesystems = append(filesystems, fs)
|
|
}
|
|
|
|
if len(filesystems) > 0 {
|
|
d.Set("filesystem", filesystems)
|
|
}
|
|
|
|
// lookup interfaces with addresses
|
|
ifacesWithAddr, err := domainGetIfacesInfo(virConn, domain, d)
|
|
if err != nil {
|
|
return diag.Errorf("error retrieving interface addresses: %s", err)
|
|
}
|
|
|
|
addressesForMac := func(mac string) []string {
|
|
// look for an ip address and try to match it with the mac address
|
|
// not sure if using the target device name is a better idea here
|
|
var addrs []string
|
|
for _, ifaceWithAddr := range ifacesWithAddr {
|
|
if len(ifaceWithAddr.Hwaddr) > 0 && strings.ToUpper(ifaceWithAddr.Hwaddr[0]) == mac {
|
|
for _, addr := range ifaceWithAddr.Addrs {
|
|
addrs = append(addrs, addr.Addr)
|
|
}
|
|
}
|
|
}
|
|
return addrs
|
|
}
|
|
|
|
var netIfaces []map[string]interface{}
|
|
for i, networkInterfaceDef := range domainDef.Devices.Interfaces {
|
|
// we need it to read old values
|
|
prefix := fmt.Sprintf("network_interface.%d", i)
|
|
|
|
mac := strings.ToUpper(networkInterfaceDef.MAC.Address)
|
|
netIface := map[string]interface{}{
|
|
"network_id": "",
|
|
"network_name": "",
|
|
"bridge": "",
|
|
"vepa": "",
|
|
"macvtap": "",
|
|
"passthrough": "",
|
|
"mac": mac,
|
|
"hostname": "",
|
|
"wait_for_lease": false,
|
|
}
|
|
|
|
netIface["wait_for_lease"] = d.Get(prefix + ".wait_for_lease").(bool)
|
|
netIface["hostname"] = d.Get(prefix + ".hostname").(string)
|
|
netIface["addresses"] = addressesForMac(mac)
|
|
log.Printf("[DEBUG] read: addresses for '%s': %+v", mac, netIface["addresses"])
|
|
|
|
if networkInterfaceDef.Source.Network != nil {
|
|
network, err := virConn.NetworkLookupByName(networkInterfaceDef.Source.Network.Network)
|
|
if err != nil {
|
|
return diag.Errorf("can't retrieve network ID for '%s'", networkInterfaceDef.Source.Network.Network)
|
|
}
|
|
|
|
netIface["network_id"] = uuidString(network.UUID)
|
|
if err != nil {
|
|
return diag.Errorf("can't retrieve network ID for '%s'", networkInterfaceDef.Source.Network.Network)
|
|
}
|
|
|
|
networkDef, err := getXMLNetworkDefFromLibvirt(virConn, network)
|
|
if err != nil {
|
|
return diag.FromErr(err)
|
|
}
|
|
|
|
netIface["network_name"] = networkInterfaceDef.Source.Network.Network
|
|
|
|
// try to look for this MAC in the DHCP configuration for this VM
|
|
if HasDHCP(networkDef) {
|
|
hostnameSearch:
|
|
for _, ip := range networkDef.IPs {
|
|
if ip.DHCP != nil {
|
|
for _, host := range ip.DHCP.Hosts {
|
|
if strings.ToUpper(host.MAC) == netIface["mac"] {
|
|
log.Printf("[DEBUG] read: hostname for '%s': '%s'", netIface["mac"], host.Name)
|
|
netIface["hostname"] = host.Name
|
|
break hostnameSearch
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else if networkInterfaceDef.Source.Bridge != nil {
|
|
netIface["bridge"] = networkInterfaceDef.Source.Bridge.Bridge
|
|
} else if networkInterfaceDef.Source.Direct != nil {
|
|
switch networkInterfaceDef.Source.Direct.Mode {
|
|
case "vepa":
|
|
netIface["vepa"] = networkInterfaceDef.Source.Direct.Dev
|
|
case "bridge":
|
|
netIface["macvtap"] = networkInterfaceDef.Source.Direct.Dev
|
|
case "passthrough":
|
|
netIface["passthrough"] = networkInterfaceDef.Source.Direct.Dev
|
|
}
|
|
}
|
|
netIfaces = append(netIfaces, netIface)
|
|
}
|
|
log.Printf("[DEBUG] read: ifaces for '%s':\n%s", domainDef.Name, spew.Sdump(netIfaces))
|
|
|
|
if len(netIfaces) > 0 {
|
|
d.Set("network_interface", netIfaces)
|
|
}
|
|
|
|
if len(ifacesWithAddr) > 0 {
|
|
d.SetConnInfo(map[string]string{
|
|
"type": "ssh",
|
|
"host": ifacesWithAddr[0].Addrs[0].Addr,
|
|
})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func resourceLibvirtDomainDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
|
|
log.Printf("[DEBUG] Delete resource libvirt_domain")
|
|
|
|
virConn := meta.(*Client).libvirt
|
|
|
|
log.Printf("[DEBUG] Deleting domain %s", d.Id())
|
|
|
|
uuid := parseUUID(d.Id())
|
|
|
|
domain, err := virConn.DomainLookupByUUID(uuid)
|
|
if err != nil {
|
|
return diag.Errorf("error retrieving libvirt domain by delete: %s", err)
|
|
}
|
|
|
|
xmlDesc, err := virConn.DomainGetXMLDesc(domain, 0)
|
|
if err != nil {
|
|
return diag.Errorf("error retrieving libvirt domain XML description: %s", err)
|
|
}
|
|
|
|
domainDef, err := newDomainDefForConnection(virConn, d)
|
|
if err != nil {
|
|
return diag.FromErr(err)
|
|
}
|
|
|
|
err = xml.Unmarshal([]byte(xmlDesc), &domainDef)
|
|
if err != nil {
|
|
return diag.Errorf("error reading libvirt domain XML description: %s", err)
|
|
}
|
|
|
|
state, _, err := virConn.DomainGetState(domain, 0)
|
|
if err != nil {
|
|
return diag.Errorf("couldn't get info about domain: %s", err)
|
|
}
|
|
|
|
if state == int32(libvirt.DomainRunning) || state == int32(libvirt.DomainPaused) {
|
|
if err := virConn.DomainDestroy(domain); err != nil {
|
|
return diag.Errorf("couldn't destroy libvirt domain: %s", err)
|
|
}
|
|
}
|
|
|
|
if err := virConn.DomainUndefineFlags(domain, libvirt.DomainUndefineNvram|
|
|
libvirt.DomainUndefineSnapshotsMetadata|libvirt.DomainUndefineManagedSave|
|
|
libvirt.DomainUndefineCheckpointsMetadata); err != nil {
|
|
if isError(err, libvirt.ErrNoSupport) || isError(err, libvirt.ErrInvalidArg) {
|
|
log.Printf("libvirt does not support undefine flags: will try again without flags")
|
|
if err := virConn.DomainUndefine(domain); err != nil {
|
|
return diag.Errorf("couldn't undefine libvirt domain: %s", err)
|
|
}
|
|
} else {
|
|
return diag.Errorf("couldn't undefine libvirt domain with flags: %s", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|