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