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/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/imagedata/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/imagedata/requests.go new file mode 100644 index 000000000..b1aac8eb0 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/imagedata/requests.go @@ -0,0 +1,28 @@ +package imagedata + +import ( + "io" + "net/http" + + "github.com/gophercloud/gophercloud" +) + +// Upload uploads image file +func Upload(client *gophercloud.ServiceClient, id string, data io.ReadSeeker) (r UploadResult) { + _, r.Err = client.Put(uploadURL(client, id), data, nil, &gophercloud.RequestOpts{ + MoreHeaders: map[string]string{"Content-Type": "application/octet-stream"}, + OkCodes: []int{204}, + }) + return +} + +// Download retrieves file +func Download(client *gophercloud.ServiceClient, id string) (r DownloadResult) { + var resp *http.Response + resp, r.Err = client.Get(downloadURL(client, id), nil, nil) + if resp != nil { + r.Body = resp.Body + r.Header = resp.Header + } + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/imagedata/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/imagedata/results.go new file mode 100644 index 000000000..970b226f2 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/imagedata/results.go @@ -0,0 +1,26 @@ +package imagedata + +import ( + "fmt" + "io" + + "github.com/gophercloud/gophercloud" +) + +// UploadResult is the result of an upload image operation +type UploadResult struct { + gophercloud.ErrResult +} + +// DownloadResult is the result of a download image operation +type DownloadResult struct { + gophercloud.Result +} + +// Extract builds images model from io.Reader +func (r DownloadResult) Extract() (io.Reader, error) { + if r, ok := r.Body.(io.Reader); ok { + return r, nil + } + return nil, fmt.Errorf("Expected io.Reader but got: %T(%#v)", r.Body, r.Body) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/imagedata/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/imagedata/urls.go new file mode 100644 index 000000000..ccd6416e5 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/imagedata/urls.go @@ -0,0 +1,13 @@ +package imagedata + +import "github.com/gophercloud/gophercloud" + +// `imageDataURL(c,i)` is the URL for the binary image data for the +// image identified by ID `i` in the service `c`. +func uploadURL(c *gophercloud.ServiceClient, imageID string) string { + return c.ServiceURL("images", imageID, "file") +} + +func downloadURL(c *gophercloud.ServiceClient, imageID string) string { + return uploadURL(c, imageID) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/requests.go new file mode 100644 index 000000000..32f09ee95 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/requests.go @@ -0,0 +1,238 @@ +package images + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToImageListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the server attributes you want to see returned. Marker and Limit are used +// for pagination. +//http://developer.openstack.org/api-ref-image-v2.html +type ListOpts struct { + // Integer value for the limit of values to return. + Limit int `q:"limit"` + + // UUID of the server at which you want to set a marker. + Marker string `q:"marker"` + + Name string `q:"name"` + Visibility ImageVisibility `q:"visibility"` + MemberStatus ImageMemberStatus `q:"member_status"` + Owner string `q:"owner"` + Status ImageStatus `q:"status"` + SizeMin int64 `q:"size_min"` + SizeMax int64 `q:"size_max"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` + Tag string `q:"tag"` +} + +// ToImageListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToImageListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// List implements image list request +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(c) + if opts != nil { + query, err := opts.ToImageListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return ImagePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// CreateOptsBuilder describes struct types that can be accepted by the Create call. +// The CreateOpts struct in this package does. +type CreateOptsBuilder interface { + // Returns value that can be passed to json.Marshal + ToImageCreateMap() (map[string]interface{}, error) +} + +// CreateOpts implements CreateOptsBuilder +type CreateOpts struct { + // Name is the name of the new image. + Name string `json:"name" required:"true"` + + // Id is the the image ID. + ID string `json:"id,omitempty"` + + // Visibility defines who can see/use the image. + Visibility *ImageVisibility `json:"visibility,omitempty"` + + // Tags is a set of image tags. + Tags []string `json:"tags,omitempty"` + + // ContainerFormat is the format of the + // container. Valid values are ami, ari, aki, bare, and ovf. + ContainerFormat string `json:"container_format,omitempty"` + + // DiskFormat is the format of the disk. If set, + // valid values are ami, ari, aki, vhd, vmdk, raw, qcow2, vdi, + // and iso. + DiskFormat string `json:"disk_format,omitempty"` + + // MinDisk is the amount of disk space in + // GB that is required to boot the image. + MinDisk int `json:"min_disk,omitempty"` + + // MinRAM is the amount of RAM in MB that + // is required to boot the image. + MinRAM int `json:"min_ram,omitempty"` + + // protected is whether the image is not deletable. + Protected *bool `json:"protected,omitempty"` + + // properties is a set of properties, if any, that + // are associated with the image. + Properties map[string]string `json:"-,omitempty"` +} + +// ToImageCreateMap assembles a request body based on the contents of +// a CreateOpts. +func (opts CreateOpts) ToImageCreateMap() (map[string]interface{}, error) { + b, err := gophercloud.BuildRequestBody(opts, "") + if err != nil { + return nil, err + } + + if opts.Properties != nil { + for k, v := range opts.Properties { + b[k] = v + } + } + return b, nil +} + +// Create implements create image request +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToImageCreateMap() + if err != nil { + r.Err = err + return r + } + _, r.Err = client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{OkCodes: []int{201}}) + return +} + +// Delete implements image delete request +func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, id), nil) + return +} + +// Get implements image get request +func Get(client *gophercloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// Update implements image updated request +func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToImageUpdateMap() + if err != nil { + r.Err = err + return r + } + _, r.Err = client.Patch(updateURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + MoreHeaders: map[string]string{"Content-Type": "application/openstack-images-v2.1-json-patch"}, + }) + return +} + +// UpdateOptsBuilder implements UpdateOptsBuilder +type UpdateOptsBuilder interface { + // returns value implementing json.Marshaler which when marshaled matches the patch schema: + // http://specs.openstack.org/openstack/glance-specs/specs/api/v2/http-patch-image-api-v2.html + ToImageUpdateMap() ([]interface{}, error) +} + +// UpdateOpts implements UpdateOpts +type UpdateOpts []Patch + +// ToImageUpdateMap builder +func (opts UpdateOpts) ToImageUpdateMap() ([]interface{}, error) { + m := make([]interface{}, len(opts)) + for i, patch := range opts { + patchJSON := patch.ToImagePatchMap() + m[i] = patchJSON + } + return m, nil +} + +// Patch represents a single update to an existing image. Multiple updates to an image can be +// submitted at the same time. +type Patch interface { + ToImagePatchMap() map[string]interface{} +} + +// UpdateVisibility updated visibility +type UpdateVisibility struct { + Visibility ImageVisibility +} + +// ToImagePatchMap builder +func (u UpdateVisibility) ToImagePatchMap() map[string]interface{} { + return map[string]interface{}{ + "op": "replace", + "path": "/visibility", + "value": u.Visibility, + } +} + +// ReplaceImageName implements Patch +type ReplaceImageName struct { + NewName string +} + +// ToImagePatchMap builder +func (r ReplaceImageName) ToImagePatchMap() map[string]interface{} { + return map[string]interface{}{ + "op": "replace", + "path": "/name", + "value": r.NewName, + } +} + +// ReplaceImageChecksum implements Patch +type ReplaceImageChecksum struct { + Checksum string +} + +// ReplaceImageChecksum builder +func (rc ReplaceImageChecksum) ToImagePatchMap() map[string]interface{} { + return map[string]interface{}{ + "op": "replace", + "path": "/checksum", + "value": rc.Checksum, + } +} + +// ReplaceImageTags implements Patch +type ReplaceImageTags struct { + NewTags []string +} + +// ToImagePatchMap builder +func (r ReplaceImageTags) ToImagePatchMap() map[string]interface{} { + return map[string]interface{}{ + "op": "replace", + "path": "/tags", + "value": r.NewTags, + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/results.go new file mode 100644 index 000000000..ae674deaf --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/results.go @@ -0,0 +1,169 @@ +package images + +import ( + "encoding/json" + "fmt" + "reflect" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// Image model +// Does not include the literal image data; just metadata. +// returned by listing images, and by fetching a specific image. +type Image struct { + // ID is the image UUID + ID string `json:"id"` + + // Name is the human-readable display name for the image. + Name string `json:"name"` + + // Status is the image status. It can be "queued" or "active" + // See imageservice/v2/images/type.go + Status ImageStatus `json:"status"` + + // Tags is a list of image tags. Tags are arbitrarily defined strings + // attached to an image. + Tags []string `json:"tags"` + + // ContainerFormat is the format of the container. + // Valid values are ami, ari, aki, bare, and ovf. + ContainerFormat string `json:"container_format"` + + // DiskFormat is the format of the disk. + // If set, valid values are ami, ari, aki, vhd, vmdk, raw, qcow2, vdi, and iso. + DiskFormat string `json:"disk_format"` + + // MinDiskGigabytes is the amount of disk space in GB that is required to boot the image. + MinDiskGigabytes int `json:"min_disk"` + + // MinRAMMegabytes [optional] is the amount of RAM in MB that is required to boot the image. + MinRAMMegabytes int `json:"min_ram"` + + // Owner is the tenant the image belongs to. + Owner string `json:"owner"` + + // Protected is whether the image is deletable or not. + Protected bool `json:"protected"` + + // Visibility defines who can see/use the image. + Visibility ImageVisibility `json:"visibility"` + + // Checksum is the checksum of the data that's associated with the image + Checksum string `json:"checksum"` + + // SizeBytes is the size of the data that's associated with the image. + SizeBytes int64 `json:"size"` + + // Metadata is a set of 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. + Metadata map[string]string `json:"metadata"` + + // Properties is a set of key-value pairs, if any, that are associated with the image. + Properties map[string]string `json:"properties"` + + // CreatedAt is the date when the image has been created. + CreatedAt time.Time `json:"created_at"` + + // UpdatedAt is the date when the last change has been made to the image or it's properties. + UpdatedAt time.Time `json:"updated_at"` + + // File is the trailing path after the glance endpoint that represent the location + // of the image or the path to retrieve it. + File string `json:"file"` + + // Schema is the path to the JSON-schema that represent the image or image entity. + Schema string `json:"schema"` +} + +func (r *Image) UnmarshalJSON(b []byte) error { + type tmp Image + var s struct { + tmp + SizeBytes interface{} `json:"size"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = Image(s.tmp) + + switch t := s.SizeBytes.(type) { + case nil: + return nil + case float32: + r.SizeBytes = int64(t) + case float64: + r.SizeBytes = int64(t) + default: + return fmt.Errorf("Unknown type for SizeBytes: %v (value: %v)", reflect.TypeOf(t), t) + } + + return err +} + +type commonResult struct { + gophercloud.Result +} + +// Extract interprets any commonResult as an Image. +func (r commonResult) Extract() (*Image, error) { + var s *Image + err := r.ExtractInto(&s) + return s, err +} + +// CreateResult represents the result of a Create operation +type CreateResult struct { + commonResult +} + +// UpdateResult represents the result of an Update operation +type UpdateResult struct { + commonResult +} + +// GetResult represents the result of a Get operation +type GetResult struct { + commonResult +} + +//DeleteResult model +type DeleteResult struct { + gophercloud.ErrResult +} + +// ImagePage represents page +type ImagePage struct { + pagination.LinkedPageBase +} + +// IsEmpty returns true if a page contains no Images results. +func (r ImagePage) IsEmpty() (bool, error) { + images, err := ExtractImages(r) + return len(images) == 0, err +} + +// NextPageURL uses the response's embedded link reference to navigate to the next page of results. +func (r ImagePage) NextPageURL() (string, error) { + var s struct { + Next string `json:"next"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return nextPageURL(r.URL.String(), s.Next), nil +} + +// ExtractImages interprets the results of a single page from a List() call, producing a slice of Image entities. +func ExtractImages(r pagination.Page) ([]Image, error) { + var s struct { + Images []Image `json:"images"` + } + err := (r.(ImagePage)).ExtractInto(&s) + return s.Images, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/types.go b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/types.go new file mode 100644 index 000000000..086e7e5d5 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/types.go @@ -0,0 +1,75 @@ +package images + +// ImageStatus image statuses +// http://docs.openstack.org/developer/glance/statuses.html +type ImageStatus string + +const ( + // ImageStatusQueued is a status for an image which identifier has + // been reserved for an image in the image registry. + ImageStatusQueued ImageStatus = "queued" + + // ImageStatusSaving denotes that an image’s raw data is currently being uploaded to Glance + ImageStatusSaving ImageStatus = "saving" + + // ImageStatusActive denotes an image that is fully available in Glance. + ImageStatusActive ImageStatus = "active" + + // ImageStatusKilled denotes that an error occurred during the uploading + // of an image’s data, and that the image is not readable. + ImageStatusKilled ImageStatus = "killed" + + // ImageStatusDeleted is used for an image that is no longer available to use. + // The image information is retained in the image registry. + ImageStatusDeleted ImageStatus = "deleted" + + // ImageStatusPendingDelete is similar to Delete, but the image is not yet deleted. + ImageStatusPendingDelete ImageStatus = "pending_delete" + + // ImageStatusDeactivated denotes that access to image data is not allowed to any non-admin user. + ImageStatusDeactivated ImageStatus = "deactivated" +) + +// ImageVisibility denotes an image that is fully available in Glance. +// This occurs when the image data is uploaded, or the image size +// is explicitly set to zero on creation. +// According to design +// https://wiki.openstack.org/wiki/Glance-v2-community-image-visibility-design +type ImageVisibility string + +const ( + // ImageVisibilityPublic all users + ImageVisibilityPublic ImageVisibility = "public" + + // ImageVisibilityPrivate users with tenantId == tenantId(owner) + ImageVisibilityPrivate ImageVisibility = "private" + + // ImageVisibilityShared images are visible to: + // - users with tenantId == tenantId(owner) + // - users with tenantId in the member-list of the image + // - users with tenantId in the member-list with member_status == 'accepted' + ImageVisibilityShared ImageVisibility = "shared" + + // ImageVisibilityCommunity images: + // - all users can see and boot it + // - users with tenantId in the member-list of the image with member_status == 'accepted' + // have this image in their default image-list + ImageVisibilityCommunity ImageVisibility = "community" +) + +// MemberStatus is a status for adding a new member (tenant) to an image member list. +type ImageMemberStatus string + +const ( + // ImageMemberStatusAccepted is the status for an accepted image member. + ImageMemberStatusAccepted ImageMemberStatus = "accepted" + + // ImageMemberStatusPending shows that the member addition is pending + ImageMemberStatusPending ImageMemberStatus = "pending" + + // ImageMemberStatusAccepted is the status for a rejected image member + ImageMemberStatusRejected ImageMemberStatus = "rejected" + + // ImageMemberStatusAll + ImageMemberStatusAll ImageMemberStatus = "all" +) diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/urls.go new file mode 100644 index 000000000..58cb8f715 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/urls.go @@ -0,0 +1,44 @@ +package images + +import ( + "strings" + + "github.com/gophercloud/gophercloud" +) + +// `listURL` is a pure function. `listURL(c)` is a URL for which a GET +// request will respond with a list of images in the service `c`. +func listURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("images") +} + +func createURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("images") +} + +// `imageURL(c,i)` is the URL for the image identified by ID `i` in +// the service `c`. +func imageURL(c *gophercloud.ServiceClient, imageID string) string { + return c.ServiceURL("images", imageID) +} + +// `getURL(c,i)` is a URL for which a GET request will respond with +// information about the image identified by ID `i` in the service +// `c`. +func getURL(c *gophercloud.ServiceClient, imageID string) string { + return imageURL(c, imageID) +} + +func updateURL(c *gophercloud.ServiceClient, imageID string) string { + return imageURL(c, imageID) +} + +func deleteURL(c *gophercloud.ServiceClient, imageID string) string { + return imageURL(c, imageID) +} + +// builds next page full url based on current url +func nextPageURL(currentURL string, next string) string { + base := currentURL[:strings.Index(currentURL, "/images")] + return base + next +} diff --git a/vendor/vendor.json b/vendor/vendor.json index cd0b9fe2f..118672d4f 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -1433,6 +1433,18 @@ "revision": "b06120d13e262ceaf890ef38ee30898813696af0", "revisionTime": "2017-02-14T04:36:15Z" }, + { + "checksumSHA1": "ApyhoqnMmvOyj4SVmCAMEPRt0CY=", + "path": "github.com/gophercloud/gophercloud/openstack/imageservice/v2/imagedata", + "revision": "b06120d13e262ceaf890ef38ee30898813696af0", + "revisionTime": "2017-02-14T04:36:15Z" + }, + { + "checksumSHA1": "suBNiSHEsfnRAY+kGSGg0P2TaUQ=", + "path": "github.com/gophercloud/gophercloud/openstack/imageservice/v2/images", + "revision": "b06120d13e262ceaf890ef38ee30898813696af0", + "revisionTime": "2017-02-14T04:36:15Z" + }, { "checksumSHA1": "aTHxjMlfNXFJ3l2TZyvIwqt/3kM=", "path": "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls", 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 +```