terraform-provider-libvirt/libvirt/coreos_ignition_def.go

200 lines
5.3 KiB
Go

package libvirt
import (
"context"
"encoding/json"
"encoding/xml"
"fmt"
"io"
"log"
"os"
"strings"
libvirt "github.com/digitalocean/go-libvirt"
"github.com/google/uuid"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
)
type defIgnition struct {
Name string
PoolName string
Content string
// Toggle the resource to a combustion script.
// Combustion and ignition have very similar boot parameters, only the format changes:
// combustion is a shell script while ignition is JSON.
Combustion bool
}
// Creates a new cloudinit with the defaults
// the provider uses.
func newIgnitionDef() defIgnition {
ign := defIgnition{}
return ign
}
// Create a ISO file based on the contents of the CloudInit instance and
// uploads it to the libVirt pool
// Returns a string holding terraform's internal ID of this resource.
func (ign *defIgnition) CreateAndUpload(ctx context.Context, client *Client) (string, error) {
virConn := client.libvirt
pool, err := virConn.StoragePoolLookupByName(ign.PoolName)
if err != nil {
return "", fmt.Errorf("can't find storage pool '%s'", ign.PoolName)
}
client.poolMutexKV.Lock(ign.PoolName)
defer client.poolMutexKV.Unlock(ign.PoolName)
// Refresh the pool of the volume so that libvirt knows it is
// not longer in use.
if err := retry.RetryContext(ctx, resourceStateTimeout, func() *retry.RetryError {
if err := virConn.StoragePoolRefresh(pool, 0); err != nil {
return retry.RetryableError(fmt.Errorf("error refreshing pool for volume: %w", err))
}
return nil
}); err != nil {
return "", err
}
volumeDef := newDefVolume()
volumeDef.Name = ign.Name
ignFile, err := ign.createFile()
if err != nil {
return "", err
}
defer func() {
if err = os.Remove(ignFile); err != nil {
log.Printf("Error while removing tmp Ignition file: %v", err)
}
}()
img, err := newImage(ignFile)
if err != nil {
return "", err
}
size, err := img.Size()
if err != nil {
return "", err
}
volumeDef.Capacity.Unit = "B"
volumeDef.Capacity.Value = size
volumeDef.Target.Format.Type = "raw"
volumeDefXML, err := xml.Marshal(volumeDef)
if err != nil {
return "", fmt.Errorf("error serializing libvirt volume: %w", err)
}
// create the volume
volume, err := virConn.StorageVolCreateXML(pool, string(volumeDefXML), 0)
if err != nil {
return "", fmt.Errorf("error creating libvirt volume for Ignition %s: %w", ign.Name, err)
}
// upload ignition file
err = img.Import(newVolumeUploader(virConn, &volume, volumeDef.Capacity.Value), volumeDef)
if err != nil {
return "", fmt.Errorf("error while uploading ignition file %s: %w", img.String(), err)
}
if volume.Key == "" {
return "", fmt.Errorf("error retrieving volume key")
}
return ign.buildTerraformKey(volume.Key), nil
}
// create a unique ID for terraform use
// The ID is made by the volume ID (the internal one used by libvirt)
// joined by the ";" with a UUID.
func (ign *defIgnition) buildTerraformKey(volumeKey string) string {
return fmt.Sprintf("%s;%s", volumeKey, uuid.New())
}
//nolint:mnd
func getIgnitionVolumeKeyFromTerraformID(id string) (string, error) {
s := strings.SplitN(id, ";", 2)
if len(s) != 2 {
return "", fmt.Errorf("%s is not a valid key", id)
}
return s[0], nil
}
// Dumps the Ignition object - either generated by Terraform or supplied as a file -
// to a temporary ignition file.
func (ign *defIgnition) createFile() (string, error) {
log.Print("Creating Ignition temporary file")
tempFile, err := os.CreateTemp("", ign.Name)
if err != nil {
return "", fmt.Errorf("error creating tmp file: %w", err)
}
defer tempFile.Close()
var file bool
file = true
if _, err := os.Stat(ign.Content); err != nil {
var js map[string]interface{}
if errConf := json.Unmarshal([]byte(ign.Content), &js); !ign.Combustion && errConf != nil {
return "", fmt.Errorf("coreos_ignition 'content' is neither a file "+
"nor a valid json object %s", ign.Content)
}
file = false
}
if !file {
if _, err := tempFile.WriteString(ign.Content); err != nil {
return "", fmt.Errorf("cannot write Ignition object to temporary " +
"ignition file")
}
} else if file {
ignFile, err := os.Open(ign.Content)
if err != nil {
return "", fmt.Errorf("error opening supplied Ignition file %s", ign.Content)
}
defer ignFile.Close()
_, err = io.Copy(tempFile, ignFile)
if err != nil {
return "", fmt.Errorf("error copying supplied Igition file to temporary file: %s", ign.Content)
}
}
return tempFile.Name(), nil
}
// Creates a new defIgnition object from provided id.
func newIgnitionDefFromRemoteVol(virConn *libvirt.Libvirt, id string) (defIgnition, error) {
ign := defIgnition{}
key, err := getIgnitionVolumeKeyFromTerraformID(id)
if err != nil {
return ign, err
}
volume, err := virConn.StorageVolLookupByKey(key)
if err != nil {
return ign, fmt.Errorf("can't retrieve volume %s: %w", key, err)
}
ign.Name = volume.Name
if ign.Name == "" {
return ign, fmt.Errorf("error retrieving volume name from key: %s", key)
}
volPool, err := virConn.StoragePoolLookupByVolume(volume)
if err != nil {
return ign, fmt.Errorf("error retrieving pool for volume: %s", volume.Name)
}
ign.PoolName = volPool.Name
if ign.PoolName == "" {
return ign, fmt.Errorf("error retrieving pool name for volume: %s", volume.Name)
}
return ign, nil
}