From e3a6b00db0e295ca0e1feb286c9b8ce663240858 Mon Sep 17 00:00:00 2001 From: Yann DEGAT Date: Thu, 16 Feb 2017 17:36:54 +0100 Subject: [PATCH] provider/openstack: Adding Image Resource --- builtin/providers/openstack/config.go | 7 + .../import_openstack_images_image_v2_test.go | 34 ++ builtin/providers/openstack/provider.go | 1 + .../resource_openstack_images_image_v2.go | 487 ++++++++++++++++++ ...resource_openstack_images_image_v2_test.go | 146 ++++++ .../openstack/r/images_image_v2.html.markdown | 107 ++++ 6 files changed, 782 insertions(+) create mode 100644 builtin/providers/openstack/import_openstack_images_image_v2_test.go create mode 100644 builtin/providers/openstack/resource_openstack_images_image_v2.go create mode 100644 builtin/providers/openstack/resource_openstack_images_image_v2_test.go create mode 100644 website/source/docs/providers/openstack/r/images_image_v2.html.markdown diff --git a/builtin/providers/openstack/config.go b/builtin/providers/openstack/config.go index 91785f22d..eb5333f26 100644 --- a/builtin/providers/openstack/config.go +++ b/builtin/providers/openstack/config.go @@ -140,6 +140,13 @@ func (c *Config) computeV2Client(region string) (*gophercloud.ServiceClient, err }) } +func (c *Config) imageV2Client(region string) (*gophercloud.ServiceClient, error) { + return openstack.NewImageServiceV2(c.osClient, gophercloud.EndpointOpts{ + Region: region, + Availability: c.getEndpointType(), + }) +} + func (c *Config) networkingV2Client(region string) (*gophercloud.ServiceClient, error) { return openstack.NewNetworkV2(c.osClient, gophercloud.EndpointOpts{ Region: region, diff --git a/builtin/providers/openstack/import_openstack_images_image_v2_test.go b/builtin/providers/openstack/import_openstack_images_image_v2_test.go new file mode 100644 index 000000000..c73a34a11 --- /dev/null +++ b/builtin/providers/openstack/import_openstack_images_image_v2_test.go @@ -0,0 +1,34 @@ +package openstack + +import ( + "testing" + + "github.com/hashicorp/terraform/helper/resource" +) + +func TestAccImagesImageV2_importBasic(t *testing.T) { + resourceName := "openstack_images_image_v2.foo" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckImagesImageV2Destroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccImagesImageV2_basic, + }, + + resource.TestStep{ + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "region", + "local_file_path", + "image_cache_path", + "image_source_url", + }, + }, + }, + }) +} diff --git a/builtin/providers/openstack/provider.go b/builtin/providers/openstack/provider.go index f1700e9c6..1772ba531 100644 --- a/builtin/providers/openstack/provider.go +++ b/builtin/providers/openstack/provider.go @@ -147,6 +147,7 @@ func Provider() terraform.ResourceProvider { "openstack_fw_firewall_v1": resourceFWFirewallV1(), "openstack_fw_policy_v1": resourceFWPolicyV1(), "openstack_fw_rule_v1": resourceFWRuleV1(), + "openstack_images_image_v2": resourceImagesImageV2(), "openstack_lb_member_v1": resourceLBMemberV1(), "openstack_lb_monitor_v1": resourceLBMonitorV1(), "openstack_lb_pool_v1": resourceLBPoolV1(), diff --git a/builtin/providers/openstack/resource_openstack_images_image_v2.go b/builtin/providers/openstack/resource_openstack_images_image_v2.go new file mode 100644 index 000000000..dc0c6bf56 --- /dev/null +++ b/builtin/providers/openstack/resource_openstack_images_image_v2.go @@ -0,0 +1,487 @@ +package openstack + +import ( + "crypto/md5" + "encoding/hex" + "fmt" + "io" + "log" + "net/http" + "os" + "path/filepath" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/imageservice/v2/imagedata" + "github.com/gophercloud/gophercloud/openstack/imageservice/v2/images" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceImagesImageV2() *schema.Resource { + return &schema.Resource{ + Create: resourceImagesImageV2Create, + Read: resourceImagesImageV2Read, + Update: resourceImagesImageV2Update, + Delete: resourceImagesImageV2Delete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "checksum": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + + "container_format": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: resourceImagesImageV2ValidateContainerFormat, + }, + + "created_at": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + + "disk_format": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: resourceImagesImageV2ValidateDiskFormat, + }, + + "file": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + + "image_cache_path": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: fmt.Sprintf("%s/.terraform/image_cache", os.Getenv("HOME")), + }, + + "image_source_url": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ConflictsWith: []string{"local_file_path"}, + }, + + "local_file_path": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ConflictsWith: []string{"image_source_url"}, + }, + + "metadata": &schema.Schema{ + Type: schema.TypeMap, + Computed: true, + }, + + "min_disk_gb": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + ValidateFunc: validatePositiveInt, + Default: 0, + }, + + "min_ram_mb": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + ValidateFunc: validatePositiveInt, + Default: 0, + }, + + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: false, + }, + + "owner": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + + "protected": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + ForceNew: true, + Default: false, + }, + + "region": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + DefaultFunc: schema.EnvDefaultFunc("OS_REGION_NAME", ""), + }, + + "schema": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + + "size_bytes": &schema.Schema{ + Type: schema.TypeInt, + Computed: true, + }, + + "status": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + + "tags": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + + "update_at": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + + "visibility": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: false, + ValidateFunc: resourceImagesImageV2ValidateVisibility, + Default: images.ImageVisibilityPrivate, + }, + }, + } +} + +func resourceImagesImageV2Create(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + imageClient, err := config.imageV2Client(GetRegion(d)) + if err != nil { + return fmt.Errorf("Error creating OpenStack image client: %s", err) + } + + protected := d.Get("protected").(bool) + visibility := resourceImagesImageV2VisibilityFromString(d.Get("visibility").(string)) + createOpts := &images.CreateOpts{ + Name: d.Get("name").(string), + ContainerFormat: d.Get("container_format").(string), + DiskFormat: d.Get("disk_format").(string), + MinDisk: d.Get("min_disk_gb").(int), + MinRAM: d.Get("min_ram_mb").(int), + Protected: &protected, + Visibility: &visibility, + } + + if tags := d.Get("tags"); tags != nil { + ts := tags.([]interface{}) + createOpts.Tags = make([]string, len(ts)) + for _, v := range ts { + createOpts.Tags = append(createOpts.Tags, v.(string)) + } + } + + d.Partial(true) + + log.Printf("[DEBUG] Create Options: %#v", createOpts) + newImg, err := images.Create(imageClient, createOpts).Extract() + if err != nil { + return fmt.Errorf("Error creating Image: %s", err) + } + + d.SetId(newImg.ID) + + // downloading/getting image file props + imgFilePath, err := resourceImagesImageV2File(d) + if err != nil { + return fmt.Errorf("Error opening file for Image: %s", err) + + } + fileSize, fileChecksum, err := resourceImagesImageV2FileProps(imgFilePath) + if err != nil { + return fmt.Errorf("Error getting file props: %s", err) + } + + // upload + imgFile, err := os.Open(imgFilePath) + if err != nil { + return fmt.Errorf("Error opening file %q: %s", imgFilePath, err) + } + defer imgFile.Close() + log.Printf("[WARN] Uploading image %s (%d bytes). This can be pretty long.", d.Id(), fileSize) + + res := imagedata.Upload(imageClient, d.Id(), imgFile) + if res.Err != nil { + return fmt.Errorf("Error while uploading file %q: %s", imgFilePath, res.Err) + } + + //wait for active + stateConf := &resource.StateChangeConf{ + Pending: []string{string(images.ImageStatusQueued), string(images.ImageStatusSaving)}, + Target: []string{string(images.ImageStatusActive)}, + Refresh: resourceImagesImageV2RefreshFunc(imageClient, d.Id(), fileSize, fileChecksum), + Timeout: 30 * time.Minute, + Delay: 10 * time.Second, + MinTimeout: 3 * time.Second, + } + + if _, err = stateConf.WaitForState(); err != nil { + return fmt.Errorf("Error waiting for Image: %s", err) + } + + d.Partial(false) + + return resourceImagesImageV2Read(d, meta) +} + +func resourceImagesImageV2Read(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + imageClient, err := config.imageV2Client(GetRegion(d)) + if err != nil { + return fmt.Errorf("Error creating OpenStack image client: %s", err) + } + + img, err := images.Get(imageClient, d.Id()).Extract() + if err != nil { + return CheckDeleted(d, err, "image") + } + + log.Printf("[DEBUG] Retrieved Image %s: %#v", d.Id(), img) + + d.Set("owner", img.Owner) + d.Set("status", img.Status) + d.Set("file", img.File) + d.Set("schema", img.Schema) + d.Set("checksum", img.Checksum) + d.Set("size_bytes", img.SizeBytes) + d.Set("metadata", img.Metadata) + d.Set("created_at", img.CreatedAt) + d.Set("update_at", img.UpdatedAt) + d.Set("container_format", img.ContainerFormat) + d.Set("disk_format", img.DiskFormat) + d.Set("min_disk_gb", img.MinDiskGigabytes) + d.Set("min_ram_mb", img.MinRAMMegabytes) + d.Set("file", img.File) + d.Set("name", img.Name) + d.Set("protected", img.Protected) + d.Set("size_bytes", img.SizeBytes) + d.Set("tags", remove_empty(img.Tags)) + d.Set("visibility", img.Visibility) + + return nil +} + +func resourceImagesImageV2Update(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + imageClient, err := config.imageV2Client(GetRegion(d)) + if err != nil { + return fmt.Errorf("Error creating OpenStack image client: %s", err) + } + + updateOpts := make(images.UpdateOpts, 0) + + if d.HasChange("visibility") { + v := images.UpdateVisibility{Visibility: d.Get("visibility").(images.ImageVisibility)} + updateOpts = append(updateOpts, v) + } + + if d.HasChange("name") { + v := images.ReplaceImageName{NewName: d.Get("name").(string)} + updateOpts = append(updateOpts, v) + } + + if d.HasChange("tags") { + v := images.ReplaceImageTags{NewTags: d.Get("tags").([]string)} + updateOpts = append(updateOpts, v) + } + + log.Printf("[DEBUG] Update Options: %#v", updateOpts) + + _, err = images.Update(imageClient, d.Id(), updateOpts).Extract() + if err != nil { + return fmt.Errorf("Error updating image: %s", err) + } + + return resourceImagesImageV2Read(d, meta) +} + +func resourceImagesImageV2Delete(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + imageClient, err := config.imageV2Client(GetRegion(d)) + if err != nil { + return fmt.Errorf("Error creating OpenStack image client: %s", err) + } + + log.Printf("[DEBUG] Deleting Image %s", d.Id()) + if err := images.Delete(imageClient, d.Id()).Err; err != nil { + return fmt.Errorf("Error deleting Image: %s", err) + } + + d.SetId("") + return nil +} + +func resourceImagesImageV2ValidateVisibility(v interface{}, k string) (ws []string, errors []error) { + value := v.(images.ImageVisibility) + if value == images.ImageVisibilityPublic || value == images.ImageVisibilityPrivate || value == images.ImageVisibilityShared || value == images.ImageVisibilityCommunity { + return + } + + errors = append(errors, fmt.Errorf("%q must be one of %q, %q, %q, %q", k, images.ImageVisibilityPublic, images.ImageVisibilityPrivate, images.ImageVisibilityCommunity, images.ImageVisibilityShared)) + return +} + +func validatePositiveInt(v interface{}, k string) (ws []string, errors []error) { + value := v.(int) + if value > 0 { + return + } + errors = append(errors, fmt.Errorf("%q must be a positive integer", k)) + return +} + +var DiskFormats = [9]string{"ami", "ari", "aki", "vhd", "vmdk", "raw", "qcow2", "vdi", "iso"} + +func resourceImagesImageV2ValidateDiskFormat(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + for i := range DiskFormats { + if value == DiskFormats[i] { + return + } + } + errors = append(errors, fmt.Errorf("%q must be one of %v", k, DiskFormats)) + return +} + +var ContainerFormats = [9]string{"ami", "ari", "aki", "bare", "ovf"} + +func resourceImagesImageV2ValidateContainerFormat(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + for i := range ContainerFormats { + if value == ContainerFormats[i] { + return + } + } + errors = append(errors, fmt.Errorf("%q must be one of %v", k, ContainerFormats)) + return +} + +func resourceImagesImageV2VisibilityFromString(v string) images.ImageVisibility { + switch v { + case "public": + return images.ImageVisibilityPublic + case "private": + return images.ImageVisibilityPrivate + case "shared": + return images.ImageVisibilityShared + case "community": + return images.ImageVisibilityCommunity + } + + return "" +} + +func fileMD5Checksum(f *os.File) (string, error) { + hash := md5.New() + if _, err := io.Copy(hash, f); err != nil { + return "", err + } + return hex.EncodeToString(hash.Sum(nil)), nil +} + +func resourceImagesImageV2FileProps(filename string) (int64, string, error) { + var filesize int64 + var filechecksum string + + file, err := os.Open(filename) + if err != nil { + return -1, "", fmt.Errorf("Error opening file for Image: %s", err) + + } + defer file.Close() + + fstat, err := file.Stat() + if err != nil { + return -1, "", fmt.Errorf("Error reading image file %q: %s", file.Name(), err) + } + + filesize = fstat.Size() + filechecksum, err = fileMD5Checksum(file) + + if err != nil { + return -1, "", fmt.Errorf("Error computing image file %q checksum: %s", file.Name(), err) + } + + return filesize, filechecksum, nil +} + +func resourceImagesImageV2File(d *schema.ResourceData) (string, error) { + if filename := d.Get("local_file_path").(string); filename != "" { + return filename, nil + } else if furl := d.Get("image_source_url").(string); furl != "" { + dir := d.Get("image_cache_path").(string) + os.MkdirAll(dir, 0700) + filename := filepath.Join(dir, fmt.Sprintf("%x.img", md5.Sum([]byte(furl)))) + + if _, err := os.Stat(filename); err != nil { + if !os.IsNotExist(err) { + return "", fmt.Errorf("Error while trying to access file %q: %s", filename, err) + } + log.Printf("[DEBUG] File doens't exists %s. will download from %s", filename, furl) + file, err := os.Create(filename) + if err != nil { + return "", fmt.Errorf("Error creating file %q: %s", filename, err) + } + defer file.Close() + resp, err := http.Get(furl) + if err != nil { + return "", fmt.Errorf("Error downloading image from %q", furl) + } + defer resp.Body.Close() + + if _, err = io.Copy(file, resp.Body); err != nil { + return "", fmt.Errorf("Error downloading image %q to file %q: %s", furl, filename, err) + } + return filename, nil + } else { + log.Printf("[DEBUG] File exists %s", filename) + return filename, nil + } + } else { + return "", fmt.Errorf("Error in config. no file specified") + } +} + +func resourceImagesImageV2RefreshFunc(client *gophercloud.ServiceClient, id string, fileSize int64, checksum string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + img, err := images.Get(client, id).Extract() + if err != nil { + return nil, "", err + } + log.Printf("[DEBUG] OpenStack image status is: %s", img.Status) + + if img.Checksum != checksum || int64(img.SizeBytes) != fileSize { + return img, fmt.Sprintf("%s", img.Status), fmt.Errorf("Error wrong size %v or checksum %q", img.SizeBytes, img.Checksum) + } + + return img, fmt.Sprintf("%s", img.Status), nil + } +} + +func remove_empty(s []string) []string { + var r []string + for _, str := range s { + if str != "" { + r = append(r, str) + } + } + return r +} diff --git a/builtin/providers/openstack/resource_openstack_images_image_v2_test.go b/builtin/providers/openstack/resource_openstack_images_image_v2_test.go new file mode 100644 index 000000000..754c85cf7 --- /dev/null +++ b/builtin/providers/openstack/resource_openstack_images_image_v2_test.go @@ -0,0 +1,146 @@ +package openstack + +import ( + "fmt" + "testing" + + "github.com/gophercloud/gophercloud/openstack/imageservice/v2/images" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "strings" +) + +func TestAccImagesImageV2_basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckImagesImageV2Destroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccImagesImageV2_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckImagesImageV2Exists(t, "openstack_images_image_v2.foo"), + ), + }, + }, + }) +} + +func TestAccImagesImageV2_with_tags(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckImagesImageV2Destroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccImagesImageV2_with_tags, + Check: resource.ComposeTestCheckFunc( + testAccCheckImagesImageV2HasTags(t, "openstack_images_image_v2.foo"), + ), + }, + }, + }) +} + +func testAccCheckImagesImageV2Destroy(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + imageClient, err := config.imageV2Client(OS_REGION_NAME) + if err != nil { + return fmt.Errorf("(testAccCheckImagesImageV2Destroy) Error creating OpenStack Image: %s", err) + } + + for _, rs := range s.RootModule().Resources { + if rs.Type != "openstack_images_image_v2" { + continue + } + + _, err := images.Get(imageClient, rs.Primary.ID).Extract() + if err == nil { + return fmt.Errorf("Image still exists") + } + } + + return nil +} + +func testAccCheckImagesImageV2Exists(t *testing.T, n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + config := testAccProvider.Meta().(*Config) + imageClient, err := config.imageV2Client(OS_REGION_NAME) + if err != nil { + return fmt.Errorf("(testAccCheckImagesImageV2Destroy) Error creating OpenStack Image: %s", err) + } + + found, err := images.Get(imageClient, rs.Primary.ID).Extract() + if err != nil { + return err + } + + if found.ID != rs.Primary.ID { + return fmt.Errorf("Image not found") + } + + return nil + } +} + +func testAccCheckImagesImageV2HasTags(t *testing.T, n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + config := testAccProvider.Meta().(*Config) + imageClient, err := config.imageV2Client(OS_REGION_NAME) + if err != nil { + return fmt.Errorf("(testAccCheckImagesImageV2Destroy) Error creating OpenStack Image: %s", err) + } + + found, err := images.Get(imageClient, rs.Primary.ID).Extract() + if err != nil { + return err + } + + if found.ID != rs.Primary.ID { + return fmt.Errorf("Image not found") + } + + tags := strings.Join(found.Tags, "") + if tags != "foobar" && tags != "barfoo" { + return fmt.Errorf("Image tags are %#v and should be \"foo, bar\"", found.Tags) + } + return nil + } +} + +var testAccImagesImageV2_basic = ` + resource "openstack_images_image_v2" "foo" { + name = "Rancher TerraformAccTest" + image_source_url = "https://releases.rancher.com/os/latest/rancheros-openstack.img" + container_format = "bare" + disk_format = "qcow2" + }` + +var testAccImagesImageV2_with_tags = ` + resource "openstack_images_image_v2" "foo" { + name = "Rancher TerraformAccTest" + image_source_url = "https://releases.rancher.com/os/latest/rancheros-openstack.img" + container_format = "bare" + disk_format = "qcow2" + tags = ["foo","bar"] + }` diff --git a/website/source/docs/providers/openstack/r/images_image_v2.html.markdown b/website/source/docs/providers/openstack/r/images_image_v2.html.markdown new file mode 100644 index 000000000..72933adfc --- /dev/null +++ b/website/source/docs/providers/openstack/r/images_image_v2.html.markdown @@ -0,0 +1,107 @@ +--- +layout: "openstack" +page_title: "OpenStack: openstack_images_image_v2" +sidebar_current: "docs-openstack-resource-images-image-v2" +description: |- + Manages a V2 Image resource within OpenStack Glance. +--- + +# openstack\_images\_image_v2 + +Manages a V2 Image resource within OpenStack Glance. + +## Example Usage + +``` +resource "openstack_images_image_v2" "rancheros" { + name = "RancherOS" + image_source_url = "https://releases.rancher.com/os/latest/rancheros-openstack.img" + container_format = "bare" + disk_format = "qcow2" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `container_format` - (Required) The container format. Must be one of + "ami", "ari", "aki", "bare", "ovf". + +* `disk_format` - (Required) The disk format. Must be one of + "ami", "ari", "aki", "vhd", "vmdk", "raw", "qcow2", "vdi", "iso". + +* `local_file_path` - (Optional) This is the filepath of the raw image file + that will be uploaded to Glance. Conflicts with `image_source_url`. + +* `image_cache_path` - (Optional) This is the directory where the images will + be downloaded. Images will be stored with a filename corresponding to + the url's md5 hash. Defaults to "$HOME/.terraform/image_cache" + +* `image_source_url` - (Optional) This is the url of the raw image that will + be downloaded in the `image_cache_path` before being uploaded to Glance. + Glance is able to download image from internet but the `gophercloud` library + does not yet provide a way to do so. + Conflicts with `local_file_path`. + +* `min_disk_gb` - (Optional) Amount of disk space (in GB) required to boot image. + Defaults to 0. + +* `min_ram_mb` - (Optional) Amount of ram (in MB) required to boot image. + Defauts to 0. + +* `name` - (Required) The name of the image. + +* `protected` - (Optional) If true, image will not be deletable. + Defaults to false. + +* `region` - (Required) The region in which to obtain the V2 Glance client. + A Glance client is needed to create an Image that can be used with + a compute instance. If omitted, the `OS_REGION_NAME` environment variable + is used. Changing this creates a new Image. + +* `tags` - (Optional) The tags of the image. It must be a list of strings. + +* `visibility` - (Optional) The visibility of the image. Must be one of + "public", "private", "community", or "shared". + +Note: The `properties` attribute handling in the gophercloud library is currently buggy +and needs to be fixed before being implemented in this resource. + +## Attributes Reference + +The following attributes are exported: + +* `checksum` - The checksum of the data associated with the image. +* `container_format` - See Argument Reference above. +* `created_at` - The date the image was created. +* `disk_format` - See Argument Reference above. +* `file` - the trailing path after the glance + endpoint that represent the location of the image + or the path to retrieve it. +* `id` - A unique ID assigned by Glance. +* `metadata` - The metadata associated with the image. + Image metadata allow for meaningfully define the image properties + and tags. See http://docs.openstack.org/developer/glance/metadefs-concepts.html. +* `min_disk_gb` - See Argument Reference above. +* `min_ram_mb` - See Argument Reference above. +* `name` - See Argument Reference above. +* `owner` - The id of the openstack user who owns the image. +* `protected` - See Argument Reference above. +* `region` - See Argument Reference above. +* `schema` - The path to the JSON-schema that represent + the image or image +* `size_bytes` - The size in bytes of the data associated with the image. +* `status` - The status of the image. It can be "queued", "active" + or "saving". +* `tags` - See Argument Reference above. +* `update_at` - The date the image was last updated. +* `visibility` - See Argument Reference above. + +## Import + +Images can be imported using the `id`, e.g. + +``` +$ terraform import openstack_images_image_v2.rancheros 89c60255-9bd6-460c-822a-e2b959ede9d2 +```