From fdd03f8e06ec3543c79006b00ad0dbaeb0f19718 Mon Sep 17 00:00:00 2001 From: Zeeshan Ali Date: Sat, 23 Feb 2019 18:51:54 +0100 Subject: [PATCH] Add storage pool resource This patch adds an experimental resource type for libvirt storage pool. Currently only directory-based pool are supported. Fixes #435. --- libvirt/helpers_test.go | 15 ++ libvirt/pool.go | 115 ++++++++++++ libvirt/provider.go | 1 + libvirt/resource_libvirt_pool.go | 255 ++++++++++++++++++++++++++ libvirt/resource_libvirt_pool_test.go | 200 ++++++++++++++++++++ website/docs/r/pool.html.markdown | 56 ++++++ 6 files changed, 642 insertions(+) create mode 100644 libvirt/pool.go create mode 100644 libvirt/resource_libvirt_pool.go create mode 100644 libvirt/resource_libvirt_pool_test.go create mode 100644 website/docs/r/pool.html.markdown diff --git a/libvirt/helpers_test.go b/libvirt/helpers_test.go index ec7e3e096..0498ae152 100644 --- a/libvirt/helpers_test.go +++ b/libvirt/helpers_test.go @@ -71,6 +71,21 @@ func getResourceFromTerraformState(resourceName string, state *terraform.State) // ** resource specifics helpers ** +// getPoolFromTerraformState lookup pool by name and return the libvirt pool from a terraform state +func getPoolFromTerraformState(name string, state *terraform.State, virConn libvirt.Connect) (*libvirt.StoragePool, error) { + rs, err := getResourceFromTerraformState(name, state) + if err != nil { + return nil, err + } + + pool, err := virConn.LookupStoragePoolByUUIDString(rs.Primary.ID) + if err != nil { + return nil, err + } + log.Printf("[DEBUG]:The ID is %s", rs.Primary.ID) + return pool, nil +} + // getVolumeFromTerraformState lookup volume by name and return the libvirt volume from a terraform state func getVolumeFromTerraformState(name string, state *terraform.State, virConn libvirt.Connect) (*libvirt.StorageVol, error) { rs, err := getResourceFromTerraformState(name, state) diff --git a/libvirt/pool.go b/libvirt/pool.go new file mode 100644 index 000000000..e5c5b566d --- /dev/null +++ b/libvirt/pool.go @@ -0,0 +1,115 @@ +package libvirt + +import ( + "fmt" + "log" + "time" + + "github.com/hashicorp/terraform/helper/resource" + libvirt "github.com/libvirt/libvirt-go" +) + +const ( + poolExistsID = "EXISTS" + poolNotExistsID = "NOT-EXISTS" +) + +// poolExists returns "EXISTS" or "NOT-EXISTS" depending on the current pool existence +func poolExists(virConn *libvirt.Connect, uuid string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + pool, err := virConn.LookupStoragePoolByUUIDString(uuid) + if err != nil { + if err.(libvirt.Error).Code == libvirt.ERR_NO_STORAGE_POOL { + log.Printf("Pool %s does not exist", uuid) + return virConn, "NOT-EXISTS", nil + } + log.Printf("Pool %s: error: %s", uuid, err.(libvirt.Error).Message) + } + if pool != nil { + defer pool.Free() + } + return virConn, poolExistsID, err + } +} + +// poolWaitForExists waits for a storage pool to be up and timeout after 5 minutes. +func poolWaitForExists(virConn *libvirt.Connect, uuid string) error { + log.Printf("Waiting for pool %s to be active...", uuid) + stateConf := &resource.StateChangeConf{ + Pending: []string{poolNotExistsID}, + Target: []string{poolExistsID}, + Refresh: poolExists(virConn, uuid), + Timeout: 1 * time.Minute, + Delay: 5 * time.Second, + MinTimeout: 3 * time.Second, + } + + if _, err := stateConf.WaitForState(); err != nil { + log.Printf("%s", err) + return fmt.Errorf("unexpected error during pool creation operation. The operation did not complete successfully") + } + return nil +} + +// poolWaitDeleted waits for a storage pool to be removed +func poolWaitDeleted(virConn *libvirt.Connect, uuid string) error { + log.Printf("Waiting for pool %s to be deleted...", uuid) + stateConf := &resource.StateChangeConf{ + Pending: []string{poolExistsID}, + Target: []string{poolNotExistsID}, + Refresh: poolExists(virConn, uuid), + Timeout: 1 * time.Minute, + Delay: 5 * time.Second, + MinTimeout: 3 * time.Second, + } + + if _, err := stateConf.WaitForState(); err != nil { + log.Printf("%s", err) + return fmt.Errorf("unexpected error during pool destroy operation. The pool was not deleted") + } + return nil +} + +// deletePool deletes the pool identified by `uuid` from libvirt +func deletePool(client *Client, uuid string) error { + virConn := client.libvirt + if virConn == nil { + return fmt.Errorf(LibVirtConIsNil) + } + + pool, err := virConn.LookupStoragePoolByUUIDString(uuid) + if err != nil { + return fmt.Errorf("error retrieving storage pool info: %s", err) + } + + poolName, err := pool.GetName() + if err != nil { + return fmt.Errorf("error retrieving storage pool name: %s", err) + } + client.poolMutexKV.Lock(poolName) + defer client.poolMutexKV.Unlock(poolName) + + info, err := pool.GetInfo() + if err != nil { + return fmt.Errorf("error retrieving storage pool info: %s", err) + } + + if info.State != libvirt.STORAGE_POOL_INACTIVE { + err := pool.Destroy() + if err != nil { + return fmt.Errorf("error deleting storage pool: %s", err) + } + } + + err = pool.Delete(0) + if err != nil { + return fmt.Errorf("error deleting storage pool: %s", err) + } + + err = pool.Undefine() + if err != nil { + return fmt.Errorf("error deleting storage pool: %s", err) + } + + return poolWaitDeleted(client.libvirt, uuid) +} diff --git a/libvirt/provider.go b/libvirt/provider.go index e27afceb8..bf2ce19fc 100644 --- a/libvirt/provider.go +++ b/libvirt/provider.go @@ -23,6 +23,7 @@ func Provider() terraform.ResourceProvider { "libvirt_domain": resourceLibvirtDomain(), "libvirt_volume": resourceLibvirtVolume(), "libvirt_network": resourceLibvirtNetwork(), + "libvirt_pool": resourceLibvirtPool(), "libvirt_cloudinit_disk": resourceCloudInitDisk(), "libvirt_ignition": resourceIgnition(), }, diff --git a/libvirt/resource_libvirt_pool.go b/libvirt/resource_libvirt_pool.go new file mode 100644 index 000000000..6012a8cbf --- /dev/null +++ b/libvirt/resource_libvirt_pool.go @@ -0,0 +1,255 @@ +package libvirt + +import ( + "encoding/xml" + "fmt" + "log" + + "github.com/hashicorp/terraform/helper/schema" + libvirt "github.com/libvirt/libvirt-go" + "github.com/libvirt/libvirt-go-xml" +) + +func resourceLibvirtPool() *schema.Resource { + return &schema.Resource{ + Create: resourceLibvirtPoolCreate, + Read: resourceLibvirtPoolRead, + Delete: resourceLibvirtPoolDelete, + Exists: resourceLibvirtPoolExists, + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "type": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "capacity": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + ForceNew: true, + }, + "allocation": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + ForceNew: true, + }, + "available": { + Type: schema.TypeString, + Computed: true, + Optional: true, + ForceNew: true, + }, + "xml": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "xslt": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + }, + }, + }, + + // Dir-specific attributes + "path": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + }, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + } +} + +func resourceLibvirtPoolCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*Client) + if client.libvirt == nil { + return fmt.Errorf(LibVirtConIsNil) + } + + poolType := d.Get("type").(string) + if poolType != "dir" { + return fmt.Errorf("Only storage pools of type \"dir\" are supported") + } + + poolName := d.Get("name").(string) + + client.poolMutexKV.Lock(poolName) + defer client.poolMutexKV.Unlock(poolName) + + // Check whether the storage pool already exists. Its name needs to be + // unique. + if _, err := client.libvirt.LookupStoragePoolByName(poolName); err == nil { + return fmt.Errorf("storage pool '%s' already exists", poolName) + } + log.Printf("[DEBUG] Pool with name '%s' does not exist yet", poolName) + + poolPath := d.Get("path").(string) + if poolPath == "" { + return fmt.Errorf("\"path\" attribute is requires for storage pools of type \"dir\"") + } + + poolDef := libvirtxml.StoragePool{ + Type: "dir", + Name: poolName, + Target: &libvirtxml.StoragePoolTarget{ + Path: poolPath, + }, + } + data, err := xmlMarshallIndented(poolDef) + if err != nil { + return fmt.Errorf("Error serializing libvirt storage pool: %s", err) + } + log.Printf("[DEBUG] Generated XML for libvirt storage pool:\n%s", data) + + data, err = transformResourceXML(data, d) + if err != nil { + return fmt.Errorf("Error applying XSLT stylesheet: %s", err) + } + + // create the pool + pool, err := client.libvirt.StoragePoolDefineXML(data, 0) + if err != nil { + return fmt.Errorf("Error creating libvirt storage pool: %s", err) + } + defer pool.Free() + + err = pool.Build(0) + if err != nil { + return fmt.Errorf("Error building libvirt storage pool: %s", err) + } + + err = pool.SetAutostart(true) + if err != nil { + return fmt.Errorf("Error setting up libvirt storage pool: %s", err) + } + + err = pool.Create(0) + if err != nil { + return fmt.Errorf("Error starting libvirt storage pool: %s", err) + } + + err = pool.Refresh(0) + if err != nil { + return fmt.Errorf("Error refreshing libvirt storage pool: %s", err) + } + + id, err := pool.GetUUIDString() + if err != nil { + return fmt.Errorf("Error retrieving libvirt pool id: %s", err) + } + d.SetId(id) + + // make sure we record the id even if the rest of this gets interrupted + d.Partial(true) + d.Set("id", id) + d.SetPartial("id") + d.Partial(false) + + log.Printf("[INFO] Pool ID: %s", d.Id()) + + if err := poolWaitForExists(client.libvirt, id); err != nil { + return err + } + + return resourceLibvirtPoolRead(d, meta) +} + +func resourceLibvirtPoolRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*Client) + virConn := client.libvirt + if virConn == nil { + return fmt.Errorf(LibVirtConIsNil) + } + + pool, err := virConn.LookupStoragePoolByUUIDString(d.Id()) + if pool == nil { + log.Printf("storage pool '%s' may have been deleted outside Terraform", d.Id()) + d.SetId("") + return nil + } + defer pool.Free() + + poolName, err := pool.GetName() + if err != nil { + return fmt.Errorf("error retrieving pool name: %s", err) + } + d.Set("name", poolName) + + info, err := pool.GetInfo() + if err != nil { + return fmt.Errorf("error retrieving pool info: %s", err) + } + d.Set("capacity", info.Capacity) + d.Set("allocation", info.Allocation) + d.Set("available", info.Available) + + poolDefXML, err := pool.GetXMLDesc(0) + if err != nil { + return fmt.Errorf("could not get XML description for pool %s: %s", poolName, err) + } + + var poolDef libvirtxml.StoragePool + err = xml.Unmarshal([]byte(poolDefXML), &poolDef) + if err != nil { + return fmt.Errorf("could not get a pool definition from XML for %s: %s", poolDef.Name, err) + } + + var poolPath string + if poolDef.Target != nil && poolDef.Target.Path != "" { + poolPath = poolDef.Target.Path + } + + if poolPath == "" { + log.Printf("Pool %s has no path specified", poolName) + } else { + log.Printf("[DEBUG] Pool %s path: %s", poolName, poolPath) + d.Set("path", poolPath) + } + + return nil +} + +func resourceLibvirtPoolDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*Client) + if client.libvirt == nil { + return fmt.Errorf(LibVirtConIsNil) + } + + return deletePool(client, d.Id()) +} + +func resourceLibvirtPoolExists(d *schema.ResourceData, meta interface{}) (bool, error) { + log.Printf("[DEBUG] Check if resource libvirt_pool exists") + client := meta.(*Client) + virConn := client.libvirt + if virConn == nil { + return false, fmt.Errorf(LibVirtConIsNil) + } + + pool, err := virConn.LookupStoragePoolByUUIDString(d.Id()) + if err != nil { + virErr := err.(libvirt.Error) + if virErr.Code != libvirt.ERR_NO_STORAGE_POOL { + return false, fmt.Errorf("Can't retrieve pool %s", d.Id()) + } + // does not exist, but no error + return false, nil + } + defer pool.Free() + + return true, nil +} diff --git a/libvirt/resource_libvirt_pool_test.go b/libvirt/resource_libvirt_pool_test.go new file mode 100644 index 000000000..6b3fa3f2a --- /dev/null +++ b/libvirt/resource_libvirt_pool_test.go @@ -0,0 +1,200 @@ +package libvirt + +import ( + "fmt" + "regexp" + "testing" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + libvirt "github.com/libvirt/libvirt-go" +) + +func testAccCheckLibvirtPoolExists(name string, pool *libvirt.StoragePool) resource.TestCheckFunc { + return func(state *terraform.State) error { + virConn := testAccProvider.Meta().(*Client).libvirt + + rs, err := getResourceFromTerraformState(name, state) + if err != nil { + return fmt.Errorf("Failed to get resource: %s", err) + } + + retrievedPool, err := getPoolFromTerraformState(name, state, *virConn) + if err != nil { + return fmt.Errorf("Failed to get pool: %s", err) + } + + realID, err := retrievedPool.GetUUIDString() + if err != nil { + return fmt.Errorf("Failed to get UUID: %s", err) + } + + if realID != rs.Primary.ID { + return fmt.Errorf("Resource ID and pool ID does not match") + } + + *pool = *retrievedPool + + return nil + } +} + +func testAccCheckLibvirtPoolDoesNotExists(n string, pool *libvirt.StoragePool) resource.TestCheckFunc { + return func(s *terraform.State) error { + virConn := testAccProvider.Meta().(*Client).libvirt + + id, err := pool.GetUUIDString() + if err != nil { + return fmt.Errorf("Can't retrieve pool ID: %s", err) + } + + pool, err := virConn.LookupStoragePoolByUUIDString(id) + if err == nil { + pool.Free() + return fmt.Errorf("Pool '%s' still exists", id) + } + + return nil + } +} + +func TestAccLibvirtPool_Basic(t *testing.T) { + var pool libvirt.StoragePool + randomPoolResource := acctest.RandString(10) + randomPoolName := acctest.RandString(10) + poolPath := "/tmp/cluster-api-provider-libvirt-pool-" + randomPoolName + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckLibvirtPoolDestroy, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` + resource "libvirt_pool" "%s" { + name = "%s" + type = "dir" + path = "%s" + }`, randomPoolResource, randomPoolName, poolPath), + Check: resource.ComposeTestCheckFunc( + testAccCheckLibvirtPoolExists("libvirt_pool."+randomPoolResource, &pool), + resource.TestCheckResourceAttr( + "libvirt_pool."+randomPoolResource, "name", randomPoolName), + resource.TestCheckResourceAttr( + "libvirt_pool."+randomPoolResource, "path", poolPath), + ), + }, + }, + }) +} + +// The destroy function should always handle the case where the resource might already be destroyed +// (manually, for example). If the resource is already destroyed, this should not return an error. +// This allows Terraform users to manually delete resources without breaking Terraform. +// This test should fail without a proper "Exists" implementation +func TestAccLibvirtPool_ManuallyDestroyed(t *testing.T) { + var pool libvirt.StoragePool + randomPoolResource := acctest.RandString(10) + randomPoolName := acctest.RandString(10) + poolPath := "/tmp/cluster-api-provider-libvirt-pool-" + randomPoolName + testAccCheckLibvirtPoolConfigBasic := fmt.Sprintf(` + resource "libvirt_pool" "%s" { + name = "%s" + type = "dir" + path = "%s" + }`, randomPoolResource, randomPoolName, poolPath) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckLibvirtPoolDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckLibvirtPoolConfigBasic, + Check: resource.ComposeTestCheckFunc( + testAccCheckLibvirtPoolExists("libvirt_pool."+randomPoolResource, &pool), + ), + }, + { + Config: testAccCheckLibvirtPoolConfigBasic, + Destroy: true, + PreConfig: func() { + client := testAccProvider.Meta().(*Client) + id, err := pool.GetUUIDString() + if err != nil { + panic(err) + } + deletePool(client, id) + }, + }, + }, + }) +} + +func TestAccLibvirtPool_UniqueName(t *testing.T) { + randomPoolName := acctest.RandString(10) + randomPoolResource2 := acctest.RandString(10) + randomPoolResource := acctest.RandString(10) + poolPath := "/tmp/cluster-api-provider-libvirt-pool-" + randomPoolName + poolPath2 := "/tmp/cluster-api-provider-libvirt-pool-" + randomPoolName + "-2" + config := fmt.Sprintf(` + resource "libvirt_pool" "%s" { + name = "%s" + type = "dir" + path = "%s" + } + + resource "libvirt_pool" "%s" { + name = "%s" + type = "dir" + path = "%s" + } + `, randomPoolResource, randomPoolName, poolPath, randomPoolResource2, randomPoolName, poolPath2) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckLibvirtPoolDestroy, + Steps: []resource.TestStep{ + { + Config: config, + ExpectError: regexp.MustCompile(`storage pool '` + randomPoolName + `' (exists already|already exists)`), + }, + }, + }) +} + +func TestAccLibvirtPool_NoDirPath(t *testing.T) { + randomPoolResource := acctest.RandString(10) + randomPoolName := acctest.RandString(10) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckLibvirtPoolDestroy, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` + resource "libvirt_pool" "%s" { + name = "%s" + type = "dir" + }`, randomPoolResource, randomPoolName), + ExpectError: regexp.MustCompile(`"path" attribute is requires for storage pools of type "dir"`), + }, + }, + }) +} + +func testAccCheckLibvirtPoolDestroy(state *terraform.State) error { + virConn := testAccProvider.Meta().(*Client).libvirt + for _, rs := range state.RootModule().Resources { + if rs.Type != "libvirt_pool" { + continue + } + _, err := virConn.LookupStoragePoolByUUIDString(rs.Primary.ID) + if err == nil { + return fmt.Errorf( + "Error waiting for pool (%s) to be destroyed: %s", + rs.Primary.ID, err) + } + } + return nil +} diff --git a/website/docs/r/pool.html.markdown b/website/docs/r/pool.html.markdown new file mode 100644 index 000000000..d0f7e4455 --- /dev/null +++ b/website/docs/r/pool.html.markdown @@ -0,0 +1,56 @@ +--- +layout: "libvirt" +page_title: "Libvirt: libvirt_pool" +sidebar_current: "docs-libvirt-pool" +description: |- + Manages a storage pool in libvirt +--- + +# libvirt\_pool + +Manages a storage pool in libvirt. Currently only directory-based storage pool are supported. For more information on +storage pools in libvirt, see [the official documentation](https://libvirt.org/formatstorage.html). + +**WARNING:** This is experimental API and may change in the future. + +## Example Usage + +```hcl +# A pool for all cluster volumes +resource "libvirt_pool" "cluster" { + name = "cluster" + type = "dir" + path = "/home/user/cluster_storage" +} + +resource "libvirt_volume" "opensuse_leap" { + name = "opensuse_leap" + pool = "${libvirt_pool.cluster.name}" + source = "http://download.opensuse.org/repositories/Cloud:/Images:/Leap_42.1/images/openSUSE-Leap-42.1-OpenStack.x86_64.qcow2" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) A unique name for the resource, required by libvirt. +* `type` - (Required) The type of the pool. Currently, only "dir" supported. +* `path` - (Optional) The directory where the pool will keep all its volumes. This is only relevant to (and required by) + the "dir" type pools. + +### Altering libvirt's generated pool XML definition + +The optional `xml` block relates to the generated pool XML. + +Currently the following attributes are supported: + +* `xslt`: specifies a XSLT stylesheet to transform the generated XML definition before creating the pool. This is used + to support features the provider does not allow to set from the schema. It is not recommended to alter properties and + settings that are exposed to the schema, as terraform will insist in changing them back to the known state. + +See the domain option with the same name for more information and examples. + +## Attributes Reference + +* `id` - a unique identifier for the resource