github.com/vtorhonen/terraform@v0.9.0-beta2.0.20170307220345-5d894e4ffda7/builtin/providers/openstack/resource_openstack_images_image_v2.go (about)

     1  package openstack
     2  
     3  import (
     4  	"crypto/md5"
     5  	"encoding/hex"
     6  	"fmt"
     7  	"io"
     8  	"log"
     9  	"net/http"
    10  	"os"
    11  	"path/filepath"
    12  	"time"
    13  
    14  	"github.com/gophercloud/gophercloud"
    15  	"github.com/gophercloud/gophercloud/openstack/imageservice/v2/imagedata"
    16  	"github.com/gophercloud/gophercloud/openstack/imageservice/v2/images"
    17  
    18  	"github.com/hashicorp/terraform/helper/resource"
    19  	"github.com/hashicorp/terraform/helper/schema"
    20  )
    21  
    22  func resourceImagesImageV2() *schema.Resource {
    23  	return &schema.Resource{
    24  		Create: resourceImagesImageV2Create,
    25  		Read:   resourceImagesImageV2Read,
    26  		Update: resourceImagesImageV2Update,
    27  		Delete: resourceImagesImageV2Delete,
    28  		Importer: &schema.ResourceImporter{
    29  			State: schema.ImportStatePassthrough,
    30  		},
    31  
    32  		Schema: map[string]*schema.Schema{
    33  			"checksum": &schema.Schema{
    34  				Type:     schema.TypeString,
    35  				Computed: true,
    36  			},
    37  
    38  			"container_format": &schema.Schema{
    39  				Type:         schema.TypeString,
    40  				Required:     true,
    41  				ForceNew:     true,
    42  				ValidateFunc: resourceImagesImageV2ValidateContainerFormat,
    43  			},
    44  
    45  			"created_at": &schema.Schema{
    46  				Type:     schema.TypeString,
    47  				Computed: true,
    48  			},
    49  
    50  			"disk_format": &schema.Schema{
    51  				Type:         schema.TypeString,
    52  				Required:     true,
    53  				ForceNew:     true,
    54  				ValidateFunc: resourceImagesImageV2ValidateDiskFormat,
    55  			},
    56  
    57  			"file": &schema.Schema{
    58  				Type:     schema.TypeString,
    59  				Computed: true,
    60  			},
    61  
    62  			"image_cache_path": &schema.Schema{
    63  				Type:     schema.TypeString,
    64  				Optional: true,
    65  				Default:  fmt.Sprintf("%s/.terraform/image_cache", os.Getenv("HOME")),
    66  			},
    67  
    68  			"image_source_url": &schema.Schema{
    69  				Type:          schema.TypeString,
    70  				Optional:      true,
    71  				ForceNew:      true,
    72  				ConflictsWith: []string{"local_file_path"},
    73  			},
    74  
    75  			"local_file_path": &schema.Schema{
    76  				Type:          schema.TypeString,
    77  				Optional:      true,
    78  				ForceNew:      true,
    79  				ConflictsWith: []string{"image_source_url"},
    80  			},
    81  
    82  			"metadata": &schema.Schema{
    83  				Type:     schema.TypeMap,
    84  				Computed: true,
    85  			},
    86  
    87  			"min_disk_gb": &schema.Schema{
    88  				Type:         schema.TypeInt,
    89  				Optional:     true,
    90  				ForceNew:     true,
    91  				ValidateFunc: validatePositiveInt,
    92  				Default:      0,
    93  			},
    94  
    95  			"min_ram_mb": &schema.Schema{
    96  				Type:         schema.TypeInt,
    97  				Optional:     true,
    98  				ForceNew:     true,
    99  				ValidateFunc: validatePositiveInt,
   100  				Default:      0,
   101  			},
   102  
   103  			"name": &schema.Schema{
   104  				Type:     schema.TypeString,
   105  				Required: true,
   106  				ForceNew: false,
   107  			},
   108  
   109  			"owner": &schema.Schema{
   110  				Type:     schema.TypeString,
   111  				Computed: true,
   112  			},
   113  
   114  			"protected": &schema.Schema{
   115  				Type:     schema.TypeBool,
   116  				Optional: true,
   117  				ForceNew: true,
   118  				Default:  false,
   119  			},
   120  
   121  			"region": &schema.Schema{
   122  				Type:        schema.TypeString,
   123  				Required:    true,
   124  				ForceNew:    true,
   125  				DefaultFunc: schema.EnvDefaultFunc("OS_REGION_NAME", ""),
   126  			},
   127  
   128  			"schema": &schema.Schema{
   129  				Type:     schema.TypeString,
   130  				Computed: true,
   131  			},
   132  
   133  			"size_bytes": &schema.Schema{
   134  				Type:     schema.TypeInt,
   135  				Computed: true,
   136  			},
   137  
   138  			"status": &schema.Schema{
   139  				Type:     schema.TypeString,
   140  				Computed: true,
   141  			},
   142  
   143  			"tags": &schema.Schema{
   144  				Type:     schema.TypeSet,
   145  				Optional: true,
   146  				Elem:     &schema.Schema{Type: schema.TypeString},
   147  				Set:      schema.HashString,
   148  			},
   149  
   150  			"update_at": &schema.Schema{
   151  				Type:     schema.TypeString,
   152  				Computed: true,
   153  			},
   154  
   155  			"visibility": &schema.Schema{
   156  				Type:         schema.TypeString,
   157  				Optional:     true,
   158  				ForceNew:     false,
   159  				ValidateFunc: resourceImagesImageV2ValidateVisibility,
   160  				Default:      "private",
   161  			},
   162  		},
   163  	}
   164  }
   165  
   166  func resourceImagesImageV2Create(d *schema.ResourceData, meta interface{}) error {
   167  	config := meta.(*Config)
   168  	imageClient, err := config.imageV2Client(GetRegion(d))
   169  	if err != nil {
   170  		return fmt.Errorf("Error creating OpenStack image client: %s", err)
   171  	}
   172  
   173  	protected := d.Get("protected").(bool)
   174  	visibility := resourceImagesImageV2VisibilityFromString(d.Get("visibility").(string))
   175  	createOpts := &images.CreateOpts{
   176  		Name:            d.Get("name").(string),
   177  		ContainerFormat: d.Get("container_format").(string),
   178  		DiskFormat:      d.Get("disk_format").(string),
   179  		MinDisk:         d.Get("min_disk_gb").(int),
   180  		MinRAM:          d.Get("min_ram_mb").(int),
   181  		Protected:       &protected,
   182  		Visibility:      &visibility,
   183  	}
   184  
   185  	if v, ok := d.GetOk("tags"); ok {
   186  		tags := v.(*schema.Set).List()
   187  		createOpts.Tags = resourceImagesImageV2BuildTags(tags)
   188  	}
   189  
   190  	d.Partial(true)
   191  
   192  	log.Printf("[DEBUG] Create Options: %#v", createOpts)
   193  	newImg, err := images.Create(imageClient, createOpts).Extract()
   194  	if err != nil {
   195  		return fmt.Errorf("Error creating Image: %s", err)
   196  	}
   197  
   198  	d.SetId(newImg.ID)
   199  
   200  	// downloading/getting image file props
   201  	imgFilePath, err := resourceImagesImageV2File(d)
   202  	if err != nil {
   203  		return fmt.Errorf("Error opening file for Image: %s", err)
   204  
   205  	}
   206  	fileSize, fileChecksum, err := resourceImagesImageV2FileProps(imgFilePath)
   207  	if err != nil {
   208  		return fmt.Errorf("Error getting file props: %s", err)
   209  	}
   210  
   211  	// upload
   212  	imgFile, err := os.Open(imgFilePath)
   213  	if err != nil {
   214  		return fmt.Errorf("Error opening file %q: %s", imgFilePath, err)
   215  	}
   216  	defer imgFile.Close()
   217  	log.Printf("[WARN] Uploading image %s (%d bytes). This can be pretty long.", d.Id(), fileSize)
   218  
   219  	res := imagedata.Upload(imageClient, d.Id(), imgFile)
   220  	if res.Err != nil {
   221  		return fmt.Errorf("Error while uploading file %q: %s", imgFilePath, res.Err)
   222  	}
   223  
   224  	//wait for active
   225  	stateConf := &resource.StateChangeConf{
   226  		Pending:    []string{string(images.ImageStatusQueued), string(images.ImageStatusSaving)},
   227  		Target:     []string{string(images.ImageStatusActive)},
   228  		Refresh:    resourceImagesImageV2RefreshFunc(imageClient, d.Id(), fileSize, fileChecksum),
   229  		Timeout:    30 * time.Minute,
   230  		Delay:      10 * time.Second,
   231  		MinTimeout: 3 * time.Second,
   232  	}
   233  
   234  	if _, err = stateConf.WaitForState(); err != nil {
   235  		return fmt.Errorf("Error waiting for Image: %s", err)
   236  	}
   237  
   238  	d.Partial(false)
   239  
   240  	return resourceImagesImageV2Read(d, meta)
   241  }
   242  
   243  func resourceImagesImageV2Read(d *schema.ResourceData, meta interface{}) error {
   244  	config := meta.(*Config)
   245  	imageClient, err := config.imageV2Client(GetRegion(d))
   246  	if err != nil {
   247  		return fmt.Errorf("Error creating OpenStack image client: %s", err)
   248  	}
   249  
   250  	img, err := images.Get(imageClient, d.Id()).Extract()
   251  	if err != nil {
   252  		return CheckDeleted(d, err, "image")
   253  	}
   254  
   255  	log.Printf("[DEBUG] Retrieved Image %s: %#v", d.Id(), img)
   256  
   257  	d.Set("owner", img.Owner)
   258  	d.Set("status", img.Status)
   259  	d.Set("file", img.File)
   260  	d.Set("schema", img.Schema)
   261  	d.Set("checksum", img.Checksum)
   262  	d.Set("size_bytes", img.SizeBytes)
   263  	d.Set("metadata", img.Metadata)
   264  	d.Set("created_at", img.CreatedAt)
   265  	d.Set("update_at", img.UpdatedAt)
   266  	d.Set("container_format", img.ContainerFormat)
   267  	d.Set("disk_format", img.DiskFormat)
   268  	d.Set("min_disk_gb", img.MinDiskGigabytes)
   269  	d.Set("min_ram_mb", img.MinRAMMegabytes)
   270  	d.Set("file", img.File)
   271  	d.Set("name", img.Name)
   272  	d.Set("protected", img.Protected)
   273  	d.Set("size_bytes", img.SizeBytes)
   274  	d.Set("tags", img.Tags)
   275  	d.Set("visibility", img.Visibility)
   276  	return nil
   277  }
   278  
   279  func resourceImagesImageV2Update(d *schema.ResourceData, meta interface{}) error {
   280  	config := meta.(*Config)
   281  	imageClient, err := config.imageV2Client(GetRegion(d))
   282  	if err != nil {
   283  		return fmt.Errorf("Error creating OpenStack image client: %s", err)
   284  	}
   285  
   286  	updateOpts := make(images.UpdateOpts, 0)
   287  
   288  	if d.HasChange("visibility") {
   289  		visibility := resourceImagesImageV2VisibilityFromString(d.Get("visibility").(string))
   290  		v := images.UpdateVisibility{Visibility: visibility}
   291  		updateOpts = append(updateOpts, v)
   292  	}
   293  
   294  	if d.HasChange("name") {
   295  		v := images.ReplaceImageName{NewName: d.Get("name").(string)}
   296  		updateOpts = append(updateOpts, v)
   297  	}
   298  
   299  	if d.HasChange("tags") {
   300  		tags := d.Get("tags").(*schema.Set).List()
   301  		v := images.ReplaceImageTags{
   302  			NewTags: resourceImagesImageV2BuildTags(tags),
   303  		}
   304  		updateOpts = append(updateOpts, v)
   305  	}
   306  
   307  	log.Printf("[DEBUG] Update Options: %#v", updateOpts)
   308  
   309  	_, err = images.Update(imageClient, d.Id(), updateOpts).Extract()
   310  	if err != nil {
   311  		return fmt.Errorf("Error updating image: %s", err)
   312  	}
   313  
   314  	return resourceImagesImageV2Read(d, meta)
   315  }
   316  
   317  func resourceImagesImageV2Delete(d *schema.ResourceData, meta interface{}) error {
   318  	config := meta.(*Config)
   319  	imageClient, err := config.imageV2Client(GetRegion(d))
   320  	if err != nil {
   321  		return fmt.Errorf("Error creating OpenStack image client: %s", err)
   322  	}
   323  
   324  	log.Printf("[DEBUG] Deleting Image %s", d.Id())
   325  	if err := images.Delete(imageClient, d.Id()).Err; err != nil {
   326  		return fmt.Errorf("Error deleting Image: %s", err)
   327  	}
   328  
   329  	d.SetId("")
   330  	return nil
   331  }
   332  
   333  func resourceImagesImageV2ValidateVisibility(v interface{}, k string) (ws []string, errors []error) {
   334  	value := v.(string)
   335  	validVisibilities := []string{
   336  		"public",
   337  		"private",
   338  		"shared",
   339  		"community",
   340  	}
   341  
   342  	for _, v := range validVisibilities {
   343  		if value == v {
   344  			return
   345  		}
   346  	}
   347  
   348  	err := fmt.Errorf("%s must be one of %s", k, validVisibilities)
   349  	errors = append(errors, err)
   350  	return
   351  }
   352  
   353  func validatePositiveInt(v interface{}, k string) (ws []string, errors []error) {
   354  	value := v.(int)
   355  	if value > 0 {
   356  		return
   357  	}
   358  	errors = append(errors, fmt.Errorf("%q must be a positive integer", k))
   359  	return
   360  }
   361  
   362  var DiskFormats = [9]string{"ami", "ari", "aki", "vhd", "vmdk", "raw", "qcow2", "vdi", "iso"}
   363  
   364  func resourceImagesImageV2ValidateDiskFormat(v interface{}, k string) (ws []string, errors []error) {
   365  	value := v.(string)
   366  	for i := range DiskFormats {
   367  		if value == DiskFormats[i] {
   368  			return
   369  		}
   370  	}
   371  	errors = append(errors, fmt.Errorf("%q must be one of %v", k, DiskFormats))
   372  	return
   373  }
   374  
   375  var ContainerFormats = [9]string{"ami", "ari", "aki", "bare", "ovf"}
   376  
   377  func resourceImagesImageV2ValidateContainerFormat(v interface{}, k string) (ws []string, errors []error) {
   378  	value := v.(string)
   379  	for i := range ContainerFormats {
   380  		if value == ContainerFormats[i] {
   381  			return
   382  		}
   383  	}
   384  	errors = append(errors, fmt.Errorf("%q must be one of %v", k, ContainerFormats))
   385  	return
   386  }
   387  
   388  func resourceImagesImageV2VisibilityFromString(v string) images.ImageVisibility {
   389  	switch v {
   390  	case "public":
   391  		return images.ImageVisibilityPublic
   392  	case "private":
   393  		return images.ImageVisibilityPrivate
   394  	case "shared":
   395  		return images.ImageVisibilityShared
   396  	case "community":
   397  		return images.ImageVisibilityCommunity
   398  	}
   399  
   400  	return ""
   401  }
   402  
   403  func fileMD5Checksum(f *os.File) (string, error) {
   404  	hash := md5.New()
   405  	if _, err := io.Copy(hash, f); err != nil {
   406  		return "", err
   407  	}
   408  	return hex.EncodeToString(hash.Sum(nil)), nil
   409  }
   410  
   411  func resourceImagesImageV2FileProps(filename string) (int64, string, error) {
   412  	var filesize int64
   413  	var filechecksum string
   414  
   415  	file, err := os.Open(filename)
   416  	if err != nil {
   417  		return -1, "", fmt.Errorf("Error opening file for Image: %s", err)
   418  
   419  	}
   420  	defer file.Close()
   421  
   422  	fstat, err := file.Stat()
   423  	if err != nil {
   424  		return -1, "", fmt.Errorf("Error reading image file %q: %s", file.Name(), err)
   425  	}
   426  
   427  	filesize = fstat.Size()
   428  	filechecksum, err = fileMD5Checksum(file)
   429  
   430  	if err != nil {
   431  		return -1, "", fmt.Errorf("Error computing image file %q checksum: %s", file.Name(), err)
   432  	}
   433  
   434  	return filesize, filechecksum, nil
   435  }
   436  
   437  func resourceImagesImageV2File(d *schema.ResourceData) (string, error) {
   438  	if filename := d.Get("local_file_path").(string); filename != "" {
   439  		return filename, nil
   440  	} else if furl := d.Get("image_source_url").(string); furl != "" {
   441  		dir := d.Get("image_cache_path").(string)
   442  		os.MkdirAll(dir, 0700)
   443  		filename := filepath.Join(dir, fmt.Sprintf("%x.img", md5.Sum([]byte(furl))))
   444  
   445  		if _, err := os.Stat(filename); err != nil {
   446  			if !os.IsNotExist(err) {
   447  				return "", fmt.Errorf("Error while trying to access file %q: %s", filename, err)
   448  			}
   449  			log.Printf("[DEBUG] File doens't exists %s. will download from %s", filename, furl)
   450  			file, err := os.Create(filename)
   451  			if err != nil {
   452  				return "", fmt.Errorf("Error creating file %q: %s", filename, err)
   453  			}
   454  			defer file.Close()
   455  			resp, err := http.Get(furl)
   456  			if err != nil {
   457  				return "", fmt.Errorf("Error downloading image from %q", furl)
   458  			}
   459  			defer resp.Body.Close()
   460  
   461  			if _, err = io.Copy(file, resp.Body); err != nil {
   462  				return "", fmt.Errorf("Error downloading image %q to file %q: %s", furl, filename, err)
   463  			}
   464  			return filename, nil
   465  		} else {
   466  			log.Printf("[DEBUG] File exists %s", filename)
   467  			return filename, nil
   468  		}
   469  	} else {
   470  		return "", fmt.Errorf("Error in config. no file specified")
   471  	}
   472  }
   473  
   474  func resourceImagesImageV2RefreshFunc(client *gophercloud.ServiceClient, id string, fileSize int64, checksum string) resource.StateRefreshFunc {
   475  	return func() (interface{}, string, error) {
   476  		img, err := images.Get(client, id).Extract()
   477  		if err != nil {
   478  			return nil, "", err
   479  		}
   480  		log.Printf("[DEBUG] OpenStack image status is: %s", img.Status)
   481  
   482  		if img.Checksum != checksum || int64(img.SizeBytes) != fileSize {
   483  			return img, fmt.Sprintf("%s", img.Status), fmt.Errorf("Error wrong size %v or checksum %q", img.SizeBytes, img.Checksum)
   484  		}
   485  
   486  		return img, fmt.Sprintf("%s", img.Status), nil
   487  	}
   488  }
   489  
   490  func resourceImagesImageV2BuildTags(v []interface{}) []string {
   491  	var tags []string
   492  	for _, tag := range v {
   493  		tags = append(tags, tag.(string))
   494  	}
   495  
   496  	return tags
   497  }