diff options
author | Flavio Castelli <flavio@castelli.name> | 2017-05-26 12:23:43 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-05-26 12:23:43 +0200 |
commit | 686015805f14b93e7ef4d6d5a5655ab2b8154b79 (patch) | |
tree | de2acf4cfdbed6636a2c854bea452f6ff33a8aae | |
parent | 72fefb6fc13e75d850a6c67d572191e829ae2143 (diff) | |
parent | f130f9848e61924bded600602a91822cf76e0959 (diff) | |
download | terraform-provider-libvirt-686015805f14b93e7ef4d6d5a5655ab2b8154b79.tar terraform-provider-libvirt-686015805f14b93e7ef4d6d5a5655ab2b8154b79.tar.gz |
Merge pull request #107 from eamonnotoole/remote-ignition-temp-file-upstream
Write Ignition file as a volume in a libvirt storage pool
-rw-r--r-- | docs/providers/libvirt/r/coreos_ignition.html.markdown | 38 | ||||
-rw-r--r-- | docs/providers/libvirt/r/domain.html.markdown | 14 | ||||
-rw-r--r-- | libvirt/coreos_ignition_def.go | 202 | ||||
-rw-r--r-- | libvirt/domain_def.go | 1 | ||||
-rw-r--r-- | libvirt/provider.go | 1 | ||||
-rw-r--r-- | libvirt/resource_libvirt_coreos_ignition.go | 95 | ||||
-rw-r--r-- | libvirt/resource_libvirt_coreos_ignition_test.go | 113 | ||||
-rw-r--r-- | libvirt/resource_libvirt_domain.go | 50 | ||||
-rw-r--r-- | libvirt/resource_libvirt_domain_test.go | 49 |
9 files changed, 490 insertions, 73 deletions
diff --git a/docs/providers/libvirt/r/coreos_ignition.html.markdown b/docs/providers/libvirt/r/coreos_ignition.html.markdown new file mode 100644 index 00000000..ae95e77b --- /dev/null +++ b/docs/providers/libvirt/r/coreos_ignition.html.markdown @@ -0,0 +1,38 @@ +--- +layout: "libvirt" +page_title: "Libvirt: libvirt_ignition" +sidebar_current: "docs-libvirt-ignition" +description: |- + Manages a CoreOS Ignition file to supply to a domain +--- + +# libvirt\_ignition + +Manages a [CoreOS Ignition](https://coreos.com/ignition/docs/latest/supported-platforms.html) +file written as a volume to a libvirt storage pool that can be used to customize a CoreOS Domain during 1st +boot. + +## Example Usage + +``` +resource "libvirt_ignition" "ignition" { + name = "example.ign" + content = <file-name or ignition object> +} + +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) A unique name for the resource, required by libvirt. +* `pool` - (Optional) The pool where the resource will be created. + If not given, the `default` pool will be used. +* `content` - (Required) This points to the source of the Ignition configuration + information that will be used to create the Ignition file in the libvirt + storage pool. The `content` can be + * The name of file that contains Ignition configuration data, or its contents + * A rendered Terraform Ignition object + +Any change of the above fields will cause a new resource to be created. diff --git a/docs/providers/libvirt/r/domain.html.markdown b/docs/providers/libvirt/r/domain.html.markdown index af3d7247..a779a69b 100644 --- a/docs/providers/libvirt/r/domain.html.markdown +++ b/docs/providers/libvirt/r/domain.html.markdown @@ -41,10 +41,9 @@ The following arguments are supported: cloud-init won't cause the domain to be recreated, however the change will have effect on the next reboot. -The following extra argument is provided for CoreOS images: - -* `coreos_ignition` - (Optional) This can be set to the name of an existing ignition -file or alternatively can be set to the rendered value of a Terraform ignition provider object. +There is an optional `coreos_ignition` parameter: +* `coreos_ignition` (Optional) The `libvirt_ignition` resource that is to be used by + the CoreOS domain. An example where a Terraform ignition provider object is used: ``` @@ -61,8 +60,13 @@ resource "ignition_config" "example" { ] } +resource "libvirt_ignition" "ignition" { + name = "ignition" + content = "${ignition_config.example.rendered}" +} + resource "libvirt_domain" "my_machine" { - coreos_ignition = "${ignition_config.example.rendered}" + coreos_ignition = "${libvirt_ignition.ignition.id}" ... } ``` diff --git a/libvirt/coreos_ignition_def.go b/libvirt/coreos_ignition_def.go new file mode 100644 index 00000000..216075f2 --- /dev/null +++ b/libvirt/coreos_ignition_def.go @@ -0,0 +1,202 @@ +package libvirt + +import ( + "encoding/xml" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "log" + "os" + "strings" + + libvirt "github.com/dmacvicar/libvirt-go" + "github.com/mitchellh/packer/common/uuid" +) + +type defIgnition struct { + Name string + PoolName string + Content string +} + +// 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(virConn *libvirt.VirConnection) (string, error) { + pool, err := virConn.LookupStoragePoolByName(ign.PoolName) + if err != nil { + return "", fmt.Errorf("can't find storage pool '%s'", ign.PoolName) + } + defer pool.Free() + + PoolSync.AcquireLock(ign.PoolName) + defer PoolSync.ReleaseLock(ign.PoolName) + + // Refresh the pool of the volume so that libvirt knows it is + // not longer in use. + WaitForSuccess("Error refreshing pool for volume", func() error { + return pool.Refresh(0) + }) + + volumeDef := newDefVolume() + volumeDef.Name = ign.Name + + ignFile, err := ign.createFile() + if err != nil { + return "", err + } + defer func() { + // Remove the tmp ignition file + if err = os.Remove(ignFile); err != nil { + log.Printf("Error while removing tmp Ignition file: %s", 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.Amount = size + volumeDef.Target.Format.Type = "raw" + + volumeDefXml, err := xml.Marshal(volumeDef) + if err != nil { + return "", fmt.Errorf("Error serializing libvirt volume: %s", err) + } + + // create the volume + volume, err := pool.StorageVolCreateXML(string(volumeDefXml), 0) + if err != nil { + return "", fmt.Errorf("Error creating libvirt volume for Ignition %s: %s", ign.Name, err) + } + defer volume.Free() + + // upload ignition file + stream, err := libvirt.NewVirStream(virConn, 0) + if err != nil { + return "", err + } + defer stream.Close() + + volume.Upload(stream, 0, uint64(volumeDef.Capacity.Amount), 0) + err = img.WriteToStream(stream) + if err != nil { + return "", err + } + + key, err := volume.GetKey() + if err != nil { + return "", fmt.Errorf("Error retrieving volume key: %s", err) + } + + return ign.buildTerraformKey(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.TimeOrderedUUID()) +} + +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 := ioutil.TempFile("", ign.Name) + if err != nil { + return "", fmt.Errorf("Cannot create tmp file for Ignition: %s", + err) + } + defer tempFile.Close() + + var file bool + file = true + if _, err := os.Stat(ign.Content); err != nil { + var js map[string]interface{} + if err_conf := json.Unmarshal([]byte(ign.Content), &js); err_conf != 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.VirConnection, id string) (defIgnition, error) { + ign := defIgnition{} + + key, err := getIgnitionVolumeKeyFromTerraformID(id) + if err != nil { + return ign, err + } + + volume, err := virConn.LookupStorageVolByKey(key) + if err != nil { + return ign, fmt.Errorf("Can't retrieve volume %s", key) + } + defer volume.Free() + + ign.Name, err = volume.GetName() + if err != nil { + return ign, fmt.Errorf("Error retrieving volume name: %s", err) + } + + volPool, err := volume.LookupPoolByVolume() + if err != nil { + return ign, fmt.Errorf("Error retrieving pool for volume: %s", err) + } + defer volPool.Free() + + ign.PoolName, err = volPool.GetName() + if err != nil { + return ign, fmt.Errorf("Error retrieving pool name: %s", err) + } + + + return ign, nil +} + diff --git a/libvirt/domain_def.go b/libvirt/domain_def.go index 65425fd7..3ac2bedb 100644 --- a/libvirt/domain_def.go +++ b/libvirt/domain_def.go @@ -63,7 +63,6 @@ type defGraphics struct { type defMetadata struct { XMLName xml.Name `xml:"http://github.com/dmacvicar/terraform-provider-libvirt/ user_data"` Xml string `xml:",cdata"` - IgnitionFile string `xml:",ignition_file,omitempty"` } type defOs struct { diff --git a/libvirt/provider.go b/libvirt/provider.go index b0f86054..08a00435 100644 --- a/libvirt/provider.go +++ b/libvirt/provider.go @@ -21,6 +21,7 @@ func Provider() terraform.ResourceProvider { "libvirt_volume": resourceLibvirtVolume(), "libvirt_network": resourceLibvirtNetwork(), "libvirt_cloudinit": resourceCloudInit(), + "libvirt_ignition": resourceIgnition(), }, ConfigureFunc: providerConfigure, diff --git a/libvirt/resource_libvirt_coreos_ignition.go b/libvirt/resource_libvirt_coreos_ignition.go new file mode 100644 index 00000000..5bacc674 --- /dev/null +++ b/libvirt/resource_libvirt_coreos_ignition.go @@ -0,0 +1,95 @@ +package libvirt + +import ( + "fmt" + "log" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceIgnition() *schema.Resource { + return &schema.Resource{ + Create: resourceIgnitionCreate, + Read: resourceIgnitionRead, + Delete: resourceIgnitionDelete, + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "pool": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "default", + ForceNew: true, + }, + "content": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + }, + } +} + +func resourceIgnitionCreate(d *schema.ResourceData, meta interface{}) error { + log.Printf("[DEBUG] creating ignition file") + virConn := meta.(*Client).libvirt + if virConn == nil { + return fmt.Errorf("The libvirt connection was nil.") + } + + ignition := newIgnitionDef() + + ignition.Name = d.Get("name").(string) + ignition.PoolName = d.Get("pool").(string) + ignition.Content = d.Get("content").(string) + + log.Printf("[INFO] ignition: %+v", ignition) + + key, err := ignition.CreateAndUpload(virConn) + if err != nil { + return err + } + d.SetId(key) + + // make sure we record the id even if the rest of this gets interrupted + d.Partial(true) // make sure we record the id even if the rest of this gets interrupted + d.Set("id", key) + d.SetPartial("id") + // TODO: at this point we have collected more things than the ID, so let's save as many things as we can + d.Partial(false) + + return resourceIgnitionRead(d, meta) +} + +func resourceIgnitionRead(d *schema.ResourceData, meta interface{}) error { + virConn := meta.(*Client).libvirt + if virConn == nil { + return fmt.Errorf("The libvirt connection was nil.") + } + + ign, err := newIgnitionDefFromRemoteVol(virConn, d.Id()) + d.Set("pool", ign.PoolName) + d.Set("name", ign.Name) + + if err != nil { + return fmt.Errorf("Error while retrieving remote volume: %s", err) + } + + return nil +} + +func resourceIgnitionDelete(d *schema.ResourceData, meta interface{}) error { + virConn := meta.(*Client).libvirt + if virConn == nil { + return fmt.Errorf("The libvirt connection was nil.") + } + + key, err := getIgnitionVolumeKeyFromTerraformID(d.Id()) + if err != nil { + return err + } + + return RemoveVolume(virConn, key) +} diff --git a/libvirt/resource_libvirt_coreos_ignition_test.go b/libvirt/resource_libvirt_coreos_ignition_test.go new file mode 100644 index 00000000..cbd5fe0e --- /dev/null +++ b/libvirt/resource_libvirt_coreos_ignition_test.go @@ -0,0 +1,113 @@ +package libvirt + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + libvirt "github.com/dmacvicar/libvirt-go" +) + +func TestAccLibvirtIgnition_Basic(t *testing.T) { + var volume libvirt.VirStorageVol + var config = fmt.Sprintf(` + resource "ignition_systemd_unit" "acceptance-test-systemd" { + name = "example.service" + content = "[Service]\nType=oneshot\nExecStart=/usr/bin/echo Hello World\n\n[Install]\nWantedBy=multi-user.target" + } + + resource "ignition_config" "acceptance-test-config" { + systemd = [ + "${ignition_systemd_unit.acceptance-test-systemd.id}", + ] + } + + resource "libvirt_ignition" "ignition" { + name = "ignition" + content = "${ignition_config.acceptance-test-config.rendered}" + } + `) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckLibvirtIgnitionDestroy, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + testAccCheckIgnitionVolumeExists("libvirt_ignition.ignition", &volume), + resource.TestCheckResourceAttr( + "libvirt_ignition.ignition", "name", "ignition"), + resource.TestCheckResourceAttr( + "libvirt_ignition.ignition", "pool", "default"), + ), + }, + }, + }) +} + +func testAccCheckIgnitionVolumeExists(n string, volume *libvirt.VirStorageVol) resource.TestCheckFunc { + return func(s *terraform.State) error { + virConn := testAccProvider.Meta().(*Client).libvirt + + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No libvirt ignition key ID is set") + } + + ignKey, err := getIgnitionVolumeKeyFromTerraformID(rs.Primary.ID) + if err != nil { + return err + } + + retrievedVol, err := virConn.LookupStorageVolByKey(ignKey) + if err != nil { + return err + } + fmt.Printf("The ID is %s", rs.Primary.ID) + + realId, err := retrievedVol.GetKey() + if err != nil { + return err + } + + if realId != ignKey { + return fmt.Errorf("Resource ID and volume key does not match") + } + + *volume = retrievedVol + return nil + } +} + +func testAccCheckLibvirtIgnitionDestroy(s *terraform.State) error { + virtConn := testAccProvider.Meta().(*Client).libvirt + + for _, rs := range s.RootModule().Resources { + if rs.Type != "libvirt_ignition" { + continue + } + + // Try to find the Ignition Volume + + ignKey, errKey := getIgnitionVolumeKeyFromTerraformID(rs.Primary.ID) + if errKey != nil { + return errKey + } + + _, err := virtConn.LookupStorageVolByKey(ignKey) + if err == nil { + return fmt.Errorf( + "Error waiting for IgnitionVolume (%s) to be destroyed: %s", + ignKey, err) + } + } + + return nil +}
\ No newline at end of file diff --git a/libvirt/resource_libvirt_domain.go b/libvirt/resource_libvirt_domain.go index 1ac3cc86..170946c1 100644 --- a/libvirt/resource_libvirt_domain.go +++ b/libvirt/resource_libvirt_domain.go @@ -1,10 +1,9 @@ package libvirt import ( - "crypto/sha256" - "encoding/hex" - "encoding/json" "encoding/xml" + "encoding/hex" + "crypto/sha256" "fmt" "log" "net" @@ -83,9 +82,8 @@ func resourceLibvirtDomain() *schema.Resource { }, "coreos_ignition": &schema.Schema{ Type: schema.TypeString, - Required: false, Optional: true, - ForceNew: false, + ForceNew: true, Default: "", }, "disk": &schema.Schema{ @@ -153,38 +151,12 @@ func resourceLibvirtDomainCreate(d *schema.ResourceData, meta interface{}) error } if ignition, ok := d.GetOk("coreos_ignition"); ok { - var file bool - file = true - ignitionString := ignition.(string) - if _, err := os.Stat(ignitionString); err != nil { - var js map[string]interface{} - if err_conf := json.Unmarshal([]byte(ignitionString), &js); err_conf != nil { - return fmt.Errorf("coreos_ignition parameter is neither a file "+ - "nor a valid json object %s", ignition) - } - log.Printf("[DEBUG] about to set file to false") - file = false - } - log.Printf("[DEBUG] file %s", file) var fw_cfg []defCmd - var ign_str string - if !file { - ignitionHash := hash(ignitionString) - tempFileName := fmt.Sprint("/tmp/", ignitionHash, ".ign") - tempFile, err := os.Create(tempFileName) - defer tempFile.Close() - if err != nil { - return fmt.Errorf("Cannot create temporary ignition file %s", tempFileName) - } - if _, err := tempFile.WriteString(ignitionString); err != nil { - return fmt.Errorf("Cannot write Ignition object to temporary "+ - "ignition file %s", tempFileName) - } - domainDef.Metadata.TerraformLibvirt.IgnitionFile = tempFileName - ign_str = fmt.Sprintf("name=opt/com.coreos/config,file=%s", tempFileName) - } else if file { - ign_str = fmt.Sprintf("name=opt/com.coreos/config,file=%s", ignitionString) + ignitionKey, err := getIgnitionVolumeKeyFromTerraformID(ignition.(string)) + if err != nil { + return err } + ign_str := fmt.Sprintf("name=opt/com.coreos/config,file=%s", ignitionKey) fw_cfg = append(fw_cfg, defCmd{"-fw_cfg"}) fw_cfg = append(fw_cfg, defCmd{ign_str}) domainDef.CmdLine.Cmd = fw_cfg @@ -828,14 +800,6 @@ func resourceLibvirtDomainDelete(d *schema.ResourceData, meta interface{}) error return fmt.Errorf("Error reading libvirt domain XML description: %s", err) } - if ignitionFile := domainDef.Metadata.TerraformLibvirt.IgnitionFile; ignitionFile != "" { - log.Printf("[DEBUG] deleting ignition file") - err = os.Remove(ignitionFile) - if err != nil && !os.IsNotExist(err) { - return fmt.Errorf("Error removing Ignition file %s: %s", ignitionFile, err) - } - } - state, err := domain.GetState() if err != nil { return fmt.Errorf("Couldn't get info about domain: %s", err) diff --git a/libvirt/resource_libvirt_domain_test.go b/libvirt/resource_libvirt_domain_test.go index 44606e16..4e584c5a 100644 --- a/libvirt/resource_libvirt_domain_test.go +++ b/libvirt/resource_libvirt_domain_test.go @@ -1,11 +1,11 @@ package libvirt import ( - "encoding/xml" "fmt" "log" "testing" + "encoding/xml" libvirt "github.com/dmacvicar/libvirt-go" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" @@ -281,6 +281,7 @@ func TestAccLibvirtDomain_Graphics(t *testing.T) { func TestAccLibvirtDomain_IgnitionObject(t *testing.T) { var domain libvirt.VirDomain + var volume libvirt.VirStorageVol var config = fmt.Sprintf(` resource "ignition_systemd_unit" "acceptance-test-systemd" { @@ -294,9 +295,14 @@ func TestAccLibvirtDomain_IgnitionObject(t *testing.T) { ] } + resource "libvirt_ignition" "ignition" { + name = "ignition" + content = "${ignition_config.acceptance-test-config.rendered}" + } + resource "libvirt_domain" "acceptance-test-domain" { name = "terraform-test-domain" - coreos_ignition = "${ignition_config.acceptance-test-config.rendered}" + coreos_ignition = "${libvirt_ignition.ignition.id}" } `) @@ -305,12 +311,12 @@ func TestAccLibvirtDomain_IgnitionObject(t *testing.T) { Providers: testAccProviders, CheckDestroy: testAccCheckLibvirtDomainDestroy, Steps: []resource.TestStep{ - resource.TestStep{ - Config: config, - ExpectNonEmptyPlan: true, + { + Config: config, Check: resource.ComposeTestCheckFunc( testAccCheckLibvirtDomainExists("libvirt_domain.acceptance-test-domain", &domain), - testAccCheckIgnitionFileNameExists(&domain), + testAccCheckIgnitionVolumeExists("libvirt_ignition.ignition", &volume), + testAccCheckIgnitionXML(&domain, &volume), ), }, }, @@ -373,36 +379,31 @@ func testAccCheckLibvirtDomainExists(n string, domain *libvirt.VirDomain) resour } } -func testAccCheckIgnitionFileNameExists(domain *libvirt.VirDomain) resource.TestCheckFunc { +func testAccCheckIgnitionXML(domain *libvirt.VirDomain, volume *libvirt.VirStorageVol) resource.TestCheckFunc { return func(s *terraform.State) error { - var ignStr string - for _, rs := range s.RootModule().Resources { - if rs.Type != "libvirt_domain" { - continue - } - ignStr = rs.Primary.Attributes["coreos_ignition"] - } - + var cmdLine []defCmd xmlDesc, err := domain.GetXMLDesc(0) if err != nil { return fmt.Errorf("Error retrieving libvirt domain XML description: %s", err) } + ignitionKey, err := volume.GetKey() + if err != nil { + return err + } + ignStr := fmt.Sprintf("name=opt/com.coreos/config,file=%s", ignitionKey) + domainDef := newDomainDef() err = xml.Unmarshal([]byte(xmlDesc), &domainDef) if err != nil { return fmt.Errorf("Error reading libvirt domain XML description: %s", err) } - ignitionFile := domainDef.Metadata.TerraformLibvirt.IgnitionFile - if ignitionFile == "" { - return fmt.Errorf("No ignition file meta-data") - } - - hashStr := hash(ignStr) - hashFile := fmt.Sprint("/tmp/", hashStr, ".ign") - if ignitionFile != hashFile { - return fmt.Errorf("Igntion file metadata incorrect %s %s", ignitionFile, hashFile) + cmdLine = domainDef.CmdLine.Cmd + for i, cmd := range cmdLine { + if i == 1 && cmd.Value != ignStr { + return fmt.Errorf("libvirt domain fw_cfg XML is incorrect %s", cmd.Value) + } } return nil } |