packer-cn/post-processor/ucloud-import/post-processor.go

349 lines
10 KiB
Go
Raw Normal View History

2019-10-12 04:46:21 -04:00
package ucloudimport
import (
"context"
"fmt"
ucloudcommon "github.com/hashicorp/packer/builder/ucloud/common"
"github.com/hashicorp/packer/common"
"github.com/hashicorp/packer/common/retry"
"github.com/hashicorp/packer/helper/config"
"github.com/hashicorp/packer/packer"
"github.com/hashicorp/packer/template/interpolate"
"github.com/ucloud/ucloud-sdk-go/services/ufile"
"github.com/ucloud/ucloud-sdk-go/services/uhost"
"github.com/ucloud/ucloud-sdk-go/ucloud"
ufsdk "github.com/ufilesdk-dev/ufile-gosdk"
"log"
"strings"
"time"
)
const (
BuilderId = "packer.post-processor.ucloud-import"
RAWFileFormat = "raw"
VHDFileFormat = "vhd"
VMDKFileFormat = "vmdk"
QCOW2FileFormat = "qcow2"
)
var regionForFileMap = ucloudcommon.NewStringConverter(map[string]string{
"cn-bj2": "cn-bj",
"cn-bj1": "cn-bj",
})
var imageFormatMap = ucloudcommon.NewStringConverter(map[string]string{
"raw": "RAW",
"vhd": "VHD",
"vmdk": "VMDK",
})
// Configuration of this post processor
type Config struct {
common.PackerConfig `mapstructure:",squash"`
ucloudcommon.AccessConfig `mapstructure:",squash"`
// Variables specific to this post processor
2019-10-18 02:52:20 -04:00
UFileBucket string `mapstructure:"ufile_bucket_name"`
UFileKey string `mapstructure:"ufile_key_name"`
SkipClean bool `mapstructure:"skip_clean"`
ImageName string `mapstructure:"image_name"`
ImageDescription string `mapstructure:"image_description"`
OSType string `mapstructure:"image_os_type"`
OSName string `mapstructure:"image_os_name"`
Format string `mapstructure:"format"`
WaitImageReadyTimeout int `mapstructure:"wait_image_ready_timeout"`
2019-10-12 04:46:21 -04:00
ctx interpolate.Context
}
type PostProcessor struct {
config Config
}
// Entry point for configuration parsing when we've defined
func (p *PostProcessor) Configure(raws ...interface{}) error {
err := config.Decode(&p.config, &config.DecodeOpts{
Interpolate: true,
InterpolateContext: &p.config.ctx,
InterpolateFilter: &interpolate.RenderFilter{
Exclude: []string{
"ufile_key_name",
},
},
}, raws...)
if err != nil {
return err
}
// Set defaults
if p.config.UFileKey == "" {
p.config.UFileKey = "packer-import-{{timestamp}}." + p.config.Format
}
2019-10-18 02:52:20 -04:00
if p.config.WaitImageReadyTimeout <= 0 {
p.config.WaitImageReadyTimeout = ucloudcommon.DefaultCreateImageTimeOut
}
2019-10-12 04:46:21 -04:00
errs := new(packer.MultiError)
// Check and render ufile_key_name
if err = interpolate.Validate(p.config.UFileKey, &p.config.ctx); err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Error parsing ufile_key_name template: %s", err))
}
// Check we have ucloud access variables defined somewhere
errs = packer.MultiErrorAppend(errs, p.config.AccessConfig.Prepare(&p.config.ctx)...)
// define all our required parameters
templates := map[string]*string{
"ufile_bucket_name": &p.config.UFileBucket,
"image_name": &p.config.ImageName,
2019-10-18 02:52:20 -04:00
"image_os_type": &p.config.OSType,
"image_os_name": &p.config.OSName,
"format": &p.config.Format,
2019-10-12 04:46:21 -04:00
}
// Check out required params are defined
for key, ptr := range templates {
if *ptr == "" {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("%s must be set", key))
}
}
2019-10-18 02:52:20 -04:00
imageName := p.config.ImageName
if !ucloudcommon.ImageNamePattern.MatchString(imageName) {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("expected %q to be 1-63 characters and only support chinese, english, numbers, '-_,.:[]', got %q", "image_name", imageName))
}
2019-10-12 04:46:21 -04:00
switch p.config.Format {
case VHDFileFormat, RAWFileFormat, VMDKFileFormat, QCOW2FileFormat:
default:
errs = packer.MultiErrorAppend(
2019-10-18 02:52:20 -04:00
errs, fmt.Errorf("expected %q only be one of 'raw', 'vhd', 'vmdk', or 'qcow2', got %q", "format", p.config.Format))
2019-10-12 04:46:21 -04:00
}
// Anything which flagged return back up the stack
if len(errs.Errors) > 0 {
return errs
}
packer.LogSecretFilter.Set(p.config.PublicKey, p.config.PrivateKey)
log.Println(p.config)
return nil
}
func (p *PostProcessor) PostProcess(ctx context.Context, ui packer.Ui, artifact packer.Artifact) (packer.Artifact, bool, bool, error) {
var err error
client, err := p.config.Client()
if err != nil {
2019-10-18 02:52:20 -04:00
return nil, false, false, fmt.Errorf("Failed to connect ucloud client %s", err)
2019-10-12 04:46:21 -04:00
}
uhostconn := client.UHostConn
ufileconn := client.UFileConn
// Render this key since we didn't in the configure phase
p.config.UFileKey, err = interpolate.Render(p.config.UFileKey, &p.config.ctx)
if err != nil {
return nil, false, false, fmt.Errorf("Error rendering ufile_key_name template: %s", err)
}
2019-10-18 02:52:20 -04:00
ui.Message(fmt.Sprintf("Rendered ufile_key_name as %s", p.config.UFileKey))
2019-10-12 04:46:21 -04:00
2019-10-18 02:52:20 -04:00
ui.Message("Looking for image in artifact")
2019-10-12 04:46:21 -04:00
// Locate the files output from the builder
var source string
for _, path := range artifact.Files() {
if strings.HasSuffix(path, "."+p.config.Format) {
source = path
break
}
}
// Hope we found something useful
if source == "" {
return nil, false, false, fmt.Errorf("No %s image file found in artifact from builder", p.config.Format)
}
2019-10-18 02:52:20 -04:00
convertedRegion := regionForFileMap.Convert(p.config.Region)
2019-10-12 04:46:21 -04:00
keyName := p.config.UFileKey
bucketName := p.config.UFileBucket
config := &ufsdk.Config{
PublicKey: p.config.PublicKey,
PrivateKey: p.config.PrivateKey,
BucketName: bucketName,
2019-10-18 02:52:20 -04:00
FileHost: fmt.Sprintf(convertedRegion + ".ufileos.com"),
2019-10-12 04:46:21 -04:00
BucketHost: "api.ucloud.cn",
}
2019-10-22 22:41:38 -04:00
// query bucket
if err := queryBucket(ufileconn, config); err != nil {
return nil, false, false, fmt.Errorf("Failed to query bucket, %s", err)
2019-10-12 04:46:21 -04:00
}
2019-10-18 02:52:20 -04:00
bucketUrl := fmt.Sprintf("http://" + bucketName + "." + convertedRegion + ".ufileos.com")
ui.Say(fmt.Sprintf("Waiting for uploading image file %s to UFile: %s/%s...", source, bucketUrl, p.config.UFileKey))
2019-10-12 04:46:21 -04:00
2019-10-18 02:52:20 -04:00
// upload file to bucket
ufileUrl, err := uploadFile(ufileconn, config, keyName, source)
2019-10-12 04:46:21 -04:00
if err != nil {
2019-10-18 02:52:20 -04:00
return nil, false, false, fmt.Errorf("Failed to Upload image file, %s", err)
2019-10-12 04:46:21 -04:00
}
2019-10-20 23:26:02 -04:00
ui.Say(fmt.Sprintf("Image file %s has been uploaded to UFile %s/%s", source, bucketUrl, p.config.UFileKey))
2019-10-12 04:46:21 -04:00
2019-10-18 02:52:20 -04:00
importImageRequest := p.buildImportImageRequest(uhostconn, ufileUrl)
2019-10-12 04:46:21 -04:00
importImageResponse, err := uhostconn.ImportCustomImage(importImageRequest)
if err != nil {
2019-10-18 02:52:20 -04:00
return nil, false, false, fmt.Errorf("Failed to import image from %s/%s, %s", bucketUrl, p.config.UFileKey, err)
2019-10-12 04:46:21 -04:00
}
ui.Say(fmt.Sprintf("Waiting for importing %s/%s to ucloud...", bucketUrl, p.config.UFileKey))
2019-10-18 02:52:20 -04:00
imageId := importImageResponse.ImageId
2019-10-12 04:46:21 -04:00
err = retry.Config{
2019-10-18 02:52:20 -04:00
StartTimeout: time.Duration(p.config.WaitImageReadyTimeout) * time.Second,
2019-10-12 04:46:21 -04:00
ShouldRetry: func(err error) bool {
return ucloudcommon.IsExpectedStateError(err)
},
2019-10-18 02:52:20 -04:00
RetryDelay: (&retry.Backoff{InitialBackoff: 2 * time.Second, MaxBackoff: 12 * time.Second, Multiplier: 2}).Linear,
2019-10-12 04:46:21 -04:00
}.Run(ctx, func(ctx context.Context) error {
image, err := client.DescribeImageById(imageId)
if err != nil {
return err
}
if image.State == ucloudcommon.ImageStateUnavailable {
2019-10-18 02:52:20 -04:00
return fmt.Errorf("Unavailable importing image %q", imageId)
2019-10-12 04:46:21 -04:00
}
if image.State != ucloudcommon.ImageStateAvailable {
return ucloudcommon.NewExpectedStateError("image", imageId)
}
return nil
})
if err != nil {
return nil, false, false, fmt.Errorf("Error on waiting for importing image %q, %s",
imageId, err.Error())
}
// Add the reported UCloud image ID to the artifact list
2019-10-18 02:52:20 -04:00
ui.Say(fmt.Sprintf("Importing created ucloud image %q in region %q Complete.", imageId, p.config.Region))
2019-10-12 04:46:21 -04:00
images := []ucloudcommon.ImageInfo{
{
ImageId: imageId,
ProjectId: p.config.ProjectId,
Region: p.config.Region,
},
}
artifact = &ucloudcommon.Artifact{
UCloudImages: ucloudcommon.NewImageInfoSet(images),
BuilderIdValue: BuilderId,
Client: client,
}
if !p.config.SkipClean {
2019-10-18 02:52:20 -04:00
ui.Message(fmt.Sprintf("Deleting import source UFile: %s/%s", p.config.UFileBucket, p.config.UFileKey))
2019-10-12 04:46:21 -04:00
if err = deleteFile(config, p.config.UFileKey); err != nil {
2019-10-18 02:52:20 -04:00
return nil, false, false, fmt.Errorf("Failed to delete UFile: %s/%s, %s", p.config.UFileBucket, p.config.UFileKey, err)
2019-10-12 04:46:21 -04:00
}
}
return artifact, false, false, nil
}
func (p *PostProcessor) buildImportImageRequest(conn *uhost.UHostClient, privateUrl string) *uhost.ImportCustomImageRequest {
req := conn.NewImportCustomImageRequest()
req.ImageName = ucloud.String(p.config.ImageName)
req.ImageDescription = ucloud.String(p.config.ImageDescription)
req.UFileUrl = ucloud.String(privateUrl)
req.OsType = ucloud.String(p.config.OSType)
req.OsName = ucloud.String(p.config.OSName)
req.Format = ucloud.String(imageFormatMap.Convert(p.config.Format))
req.Auth = ucloud.Bool(true)
return req
}
2019-10-22 22:41:38 -04:00
func queryBucket(conn *ufile.UFileClient, config *ufsdk.Config) error {
2019-10-12 04:46:21 -04:00
var limit = 100
var offset int
var bucketList []ufile.UFileBucketSet
for {
req := conn.NewDescribeBucketRequest()
req.Limit = ucloud.Int(limit)
req.Offset = ucloud.Int(offset)
resp, err := conn.DescribeBucket(req)
if err != nil {
2019-10-18 02:52:20 -04:00
return fmt.Errorf("error on reading bucket list when create bucket, %s", err)
2019-10-12 04:46:21 -04:00
}
if resp == nil || len(resp.DataSet) < 1 {
break
}
bucketList = append(bucketList, resp.DataSet...)
if len(resp.DataSet) < limit {
break
}
offset = offset + limit
}
var bucketNames []string
for _, v := range bucketList {
bucketNames = append(bucketNames, v.BucketName)
}
if !ucloudcommon.IsStringIn(config.BucketName, bucketNames) {
2019-10-22 22:41:38 -04:00
return fmt.Errorf("the bucket %s is not exit", config.BucketName)
2019-10-12 04:46:21 -04:00
}
return nil
}
2019-10-18 02:52:20 -04:00
func uploadFile(conn *ufile.UFileClient, config *ufsdk.Config, keyName, source string) (string, error) {
2019-10-12 04:46:21 -04:00
reqFile, err := ufsdk.NewFileRequest(config, nil)
if err != nil {
2019-10-18 02:52:20 -04:00
return "", fmt.Errorf("error on building upload file request, %s", err)
2019-10-12 04:46:21 -04:00
}
2019-10-18 02:52:20 -04:00
// upload file in segments
err = reqFile.AsyncMPut(source, keyName, "")
2019-10-12 04:46:21 -04:00
if err != nil {
2019-10-18 02:52:20 -04:00
return "", fmt.Errorf("error on upload file, %s, details: %s", err, reqFile.DumpResponse(true))
2019-10-12 04:46:21 -04:00
}
2019-10-18 02:52:20 -04:00
reqBucket := conn.NewDescribeBucketRequest()
reqBucket.BucketName = ucloud.String(config.BucketName)
resp, err := conn.DescribeBucket(reqBucket)
2019-10-12 04:46:21 -04:00
if err != nil {
2019-10-18 02:52:20 -04:00
return "", fmt.Errorf("error on reading bucket list when upload file, %s", err)
2019-10-12 04:46:21 -04:00
}
2019-10-18 02:52:20 -04:00
if resp.DataSet[0].Type == "private" {
2019-10-22 22:41:38 -04:00
return reqFile.GetPrivateURL(keyName, time.Duration(24*60*60)*time.Second), nil
2019-10-12 04:46:21 -04:00
}
2019-10-18 02:52:20 -04:00
return reqFile.GetPublicURL(keyName), nil
2019-10-12 04:46:21 -04:00
}
func deleteFile(config *ufsdk.Config, keyName string) error {
req, err := ufsdk.NewFileRequest(config, nil)
if err != nil {
return fmt.Errorf("error on new deleting file, %s", err)
}
req.DeleteFile(keyName)
if err != nil {
return fmt.Errorf("error on deleting file, %s", err)
}
return nil
}