From af333a5de0ea908b033a95fa77ff5c803b67f306 Mon Sep 17 00:00:00 2001 From: Gennady Lipenkov Date: Tue, 26 Mar 2019 12:29:15 +0000 Subject: [PATCH] Initial version --- builder/yandex/artifact.go | 60 ++++ builder/yandex/artifact_mini.go | 46 +++ builder/yandex/builder.go | 103 +++++++ builder/yandex/config.go | 210 ++++++++++++++ builder/yandex/config_test.go | 244 ++++++++++++++++ builder/yandex/driver_yc.go | 124 ++++++++ builder/yandex/image.go | 11 + builder/yandex/ssh.go | 10 + builder/yandex/step_create_image.go | 70 +++++ builder/yandex/step_create_instance.go | 337 ++++++++++++++++++++++ builder/yandex/step_create_ssh_key.go | 102 +++++++ builder/yandex/step_instance_info.go | 109 +++++++ builder/yandex/step_shutdown.go | 49 ++++ builder/yandex/template_func.go | 36 +++ builder/yandex/test_data/fake-sa-key.json | 9 + builder/yandex/util.go | 38 +++ 16 files changed, 1558 insertions(+) create mode 100644 builder/yandex/artifact.go create mode 100644 builder/yandex/artifact_mini.go create mode 100644 builder/yandex/builder.go create mode 100644 builder/yandex/config.go create mode 100644 builder/yandex/config_test.go create mode 100644 builder/yandex/driver_yc.go create mode 100644 builder/yandex/image.go create mode 100644 builder/yandex/ssh.go create mode 100644 builder/yandex/step_create_image.go create mode 100644 builder/yandex/step_create_instance.go create mode 100644 builder/yandex/step_create_ssh_key.go create mode 100644 builder/yandex/step_instance_info.go create mode 100644 builder/yandex/step_shutdown.go create mode 100644 builder/yandex/template_func.go create mode 100644 builder/yandex/test_data/fake-sa-key.json create mode 100644 builder/yandex/util.go diff --git a/builder/yandex/artifact.go b/builder/yandex/artifact.go new file mode 100644 index 000000000..bb6ff465b --- /dev/null +++ b/builder/yandex/artifact.go @@ -0,0 +1,60 @@ +package yandex + +import ( + "fmt" + "log" +) + +//revive:disable:var-naming + +// Artifact represents a image as the result of a Packer build. +type Artifact struct { + image *Image + driver Driver + config *Config +} + +// BuilderID returns the builder Id. +//revive:disable:var-naming +func (*Artifact) BuilderId() string { + return BuilderID +} + +// Destroy destroys the image represented by the artifact. +func (a *Artifact) Destroy() error { + log.Printf("Destroying image: %s", a.image.Name) + errCh := a.driver.DeleteImage(a.image.Name) + return errCh +} + +// Files returns the files represented by the artifact. +func (*Artifact) Files() []string { + return nil +} + +// Id returns the image name. +//revive:disable:var-naming +func (a *Artifact) Id() string { + return a.image.Name +} + +// String returns the string representation of the artifact. +func (a *Artifact) String() string { + return fmt.Sprintf("A disk image was created: %v (id: %v)", a.image.Name, a.image.ID) +} + +func (a *Artifact) State(name string) interface{} { + switch name { + case "ImageID": + return a.image.ID + case "ImageName": + return a.image.Name + case "ImageSizeGb": + return a.image.SizeGb + case "FolderID": + return a.config.FolderID + case "BuildZone": + return a.config.Zone + } + return nil +} diff --git a/builder/yandex/artifact_mini.go b/builder/yandex/artifact_mini.go new file mode 100644 index 000000000..297f9fd9f --- /dev/null +++ b/builder/yandex/artifact_mini.go @@ -0,0 +1,46 @@ +package yandex + +import "fmt" + +type ArtifactMini struct { + config *Config + imageID string + imageName string + imageFamily string +} + +//revive:disable:var-naming +func (*ArtifactMini) BuilderId() string { + return BuilderID +} + +func (a *ArtifactMini) Id() string { + return a.imageID +} + +func (*ArtifactMini) Files() []string { + return nil +} + +//revive:enable:var-naming +func (a *ArtifactMini) String() string { + return fmt.Sprintf("A disk image was created: %v (id: %v) (family: %v)", a.imageName, a.imageID, a.imageFamily) +} + +func (a *ArtifactMini) State(name string) interface{} { + switch name { + case "ImageID": + return a.imageID + case "FolderID": + return a.config.FolderID + case "BuildZone": + return a.config.Zone + } + return nil + +} + +func (*ArtifactMini) Destroy() error { + // no destroy right now + return nil +} diff --git a/builder/yandex/builder.go b/builder/yandex/builder.go new file mode 100644 index 000000000..095733a18 --- /dev/null +++ b/builder/yandex/builder.go @@ -0,0 +1,103 @@ +package yandex + +import ( + "fmt" + "log" + + "github.com/hashicorp/packer/common" + "github.com/hashicorp/packer/helper/communicator" + "github.com/hashicorp/packer/helper/multistep" + "github.com/hashicorp/packer/packer" +) + +// The unique ID for this builder. +const BuilderID = "packer.yandex" + +// Builder represents a Packer Builder. +type Builder struct { + config *Config + runner multistep.Runner +} + +// Prepare processes the build configuration parameters. +func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { + c, warnings, errs := NewConfig(raws...) + if errs != nil { + return warnings, errs + } + b.config = c + return warnings, nil +} + +// Run executes a yandex Packer build and returns a packer.Artifact +// representing a Yandex Cloud machine image. +func (b *Builder) Run(ui packer.Ui, hook packer.Hook) (packer.Artifact, error) { + driver, err := NewDriverYandexCloud(ui, b.config) + + if err != nil { + return nil, err + } + + // Set up the state + state := &multistep.BasicStateBag{} + state.Put("config", b.config) + state.Put("driver", driver) + state.Put("sdk", driver.SDK()) + state.Put("hook", hook) + state.Put("ui", ui) + + // Build the steps + steps := []multistep.Step{ + &stepCreateSSHKey{ + Debug: b.config.PackerDebug, + DebugKeyPath: fmt.Sprintf("yc_%s.pem", b.config.PackerBuildName), + }, + &stepCreateInstance{ + Debug: b.config.PackerDebug, + SerialLogFile: b.config.SerialLogFile, + }, + &stepInstanceInfo{}, + &communicator.StepConnect{ + Config: &b.config.Communicator, + Host: commHost, + SSHConfig: b.config.Communicator.SSHConfigFunc(), + }, + &common.StepProvision{}, + &common.StepCleanupTempKeys{ + Comm: &b.config.Communicator, + }, + &stepShutdown{ + Debug: b.config.PackerDebug, + }, + &stepCreateImage{}, + } + + // Run the steps + b.runner = common.NewRunner(steps, b.config.PackerConfig, ui) + b.runner.Run(state) + + // Report any errors + if rawErr, ok := state.GetOk("error"); ok { + return nil, rawErr.(error) + } + if _, ok := state.GetOk("image_id"); !ok { + log.Println("Failed to find image_id in state. Bug?") + return nil, nil + } + + artifact := &ArtifactMini{ + imageID: state.Get("image_id").(string), + imageName: state.Get("image_name").(string), + imageFamily: state.Get("image_family").(string), + config: b.config, + } + return artifact, nil +} + +// Cancel. +func (b *Builder) Cancel() { + if b.runner != nil { + log.Println("Cancelling the step runner...") + b.runner.Cancel() + } +} diff --git a/builder/yandex/config.go b/builder/yandex/config.go new file mode 100644 index 000000000..90cff475c --- /dev/null +++ b/builder/yandex/config.go @@ -0,0 +1,210 @@ +package yandex + +import ( + "errors" + "fmt" + "os" + "regexp" + "time" + + "github.com/hashicorp/packer/common" + "github.com/hashicorp/packer/common/uuid" + "github.com/hashicorp/packer/helper/communicator" + "github.com/hashicorp/packer/helper/config" + "github.com/hashicorp/packer/packer" + "github.com/hashicorp/packer/template/interpolate" + + "github.com/yandex-cloud/go-sdk/iamkey" +) + +var reImageFamily = regexp.MustCompile(`^[a-z]([-a-z0-9]{0,61}[a-z0-9])?$`) + +type Config struct { + common.PackerConfig `mapstructure:",squash"` + Communicator communicator.Config `mapstructure:",squash"` + + Endpoint string `mapstructure:"endpoint"` + Token string `mapstructure:"token"` + ServiceAccountKeyFile string `mapstructure:"service_account_key_file"` + FolderID string `mapstructure:"folder_id"` + Zone string `mapstructure:"zone"` + + SerialLogFile string `mapstructure:"serial_log_file"` + InstanceCores int `mapstructure:"instance_cores"` + InstanceMemory int `mapstructure:"instance_mem_gb"` + DiskSizeGb int `mapstructure:"disk_size_gb"` + DiskType string `mapstructure:"disk_type"` + SubnetID string `mapstructure:"subnet_id"` + ImageName string `mapstructure:"image_name"` + ImageFamily string `mapstructure:"image_family"` + ImageDescription string `mapstructure:"image_description"` + ImageLabels map[string]string `mapstructure:"image_labels"` + ImageProductIDs []string `mapstructure:"image_product_ids"` + InstanceName string `mapstructure:"instance_name"` + Labels map[string]string `mapstructure:"labels"` + DiskName string `mapstructure:"disk_name"` + MachineType string `mapstructure:"machine_type"` + Metadata map[string]string `mapstructure:"metadata"` + SourceImageID string `mapstructure:"source_image_id"` + SourceImageFamily string `mapstructure:"source_image_family"` + SourceImageFolderID string `mapstructure:"source_image_folder_id"` + UseInternalIP bool `mapstructure:"use_internal_ip"` + UseIPv4Nat bool `mapstructure:"use_ipv4_nat"` + UseIPv6 bool `mapstructure:"use_ipv6"` + + RawStepTimeout string `mapstructure:"step_timeout"` + + stepTimeout time.Duration + ctx interpolate.Context +} + +func NewConfig(raws ...interface{}) (*Config, []string, error) { + c := &Config{} + c.ctx.Funcs = TemplateFuncs + err := config.Decode(c, &config.DecodeOpts{ + Interpolate: true, + InterpolateContext: &c.ctx, + }, raws...) + if err != nil { + return nil, nil, err + } + + var errs *packer.MultiError + + if c.SerialLogFile != "" { + if _, err := os.Stat(c.SerialLogFile); os.IsExist(err) { + errs = packer.MultiErrorAppend(errs, + fmt.Errorf("Serial log file %s already exist", c.SerialLogFile)) + } + } + + if c.InstanceCores == 0 { + c.InstanceCores = 2 + } + + if c.InstanceMemory == 0 { + c.InstanceMemory = 4 + } + + if c.DiskSizeGb == 0 { + c.DiskSizeGb = 10 + } + + if c.DiskType == "" { + c.DiskType = "network-hdd" + } + + if c.ImageDescription == "" { + c.ImageDescription = "Created by Packer" + } + + if c.ImageName == "" { + img, err := interpolate.Render("packer-{{timestamp}}", nil) + if err != nil { + errs = packer.MultiErrorAppend(errs, + fmt.Errorf("Unable to render default image name: %s ", err)) + } else { + c.ImageName = img + } + } + + if len(c.ImageFamily) > 63 { + errs = packer.MultiErrorAppend(errs, + errors.New("Invalid image family: Must not be longer than 63 characters")) + } + + if c.ImageFamily != "" { + if !reImageFamily.MatchString(c.ImageFamily) { + errs = packer.MultiErrorAppend(errs, + errors.New("Invalid image family: The first character must be a "+ + "lowercase letter, and all following characters must be a dash, "+ + "lowercase letter, or digit, except the last character, which cannot be a dash")) + } + } + + if c.InstanceName == "" { + c.InstanceName = fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID()) + } + + if c.DiskName == "" { + c.DiskName = c.InstanceName + "-disk" + } + + if c.MachineType == "" { + c.MachineType = "standard-v1" + } + + if c.RawStepTimeout == "" { + c.RawStepTimeout = "5m" + } + + if es := c.Communicator.Prepare(&c.ctx); len(es) > 0 { + errs = packer.MultiErrorAppend(errs, es...) + } + + // Process required parameters. + + if c.SourceImageID == "" && c.SourceImageFamily == "" { + errs = packer.MultiErrorAppend( + errs, errors.New("a source_image_id or source_image_family must be specified")) + } + + err = c.CalcTimeout() + if err != nil { + errs = packer.MultiErrorAppend(errs, err) + } + + if c.Endpoint == "" { + c.Endpoint = "api.cloud.yandex.net:443" + } + + if c.Token == "" && c.ServiceAccountKeyFile == "" { + errs = packer.MultiErrorAppend( + errs, errors.New("a token or service account key file must be specified")) + + } + + if c.Token != "" && c.ServiceAccountKeyFile != "" { + errs = packer.MultiErrorAppend( + errs, errors.New("one of token or service account key file must be specified, not both")) + + } + + if c.Token != "" { + packer.LogSecretFilter.Set(c.Token) + } + + if c.ServiceAccountKeyFile != "" { + if _, err := iamkey.ReadFromJSONFile(c.ServiceAccountKeyFile); err != nil { + errs = packer.MultiErrorAppend( + errs, fmt.Errorf("fail to parse service account key file: %s", err)) + } + + } + + if c.FolderID == "" { + errs = packer.MultiErrorAppend( + errs, errors.New("a folder_id must be specified")) + } + + if c.Zone == "" { + errs = packer.MultiErrorAppend( + errs, errors.New("a zone must be specified")) + } + + // Check for any errors. + if errs != nil && len(errs.Errors) > 0 { + return nil, nil, errs + } + + return c, nil, nil +} + +func (c *Config) CalcTimeout() error { + stepTimeout, err := time.ParseDuration(c.RawStepTimeout) + if err != nil { + return fmt.Errorf("Failed parsing step_timeout: %s", err) + } + c.stepTimeout = stepTimeout + return nil +} diff --git a/builder/yandex/config_test.go b/builder/yandex/config_test.go new file mode 100644 index 000000000..3ac026241 --- /dev/null +++ b/builder/yandex/config_test.go @@ -0,0 +1,244 @@ +package yandex + +import ( + "io/ioutil" + "os" + "strings" + "testing" +) + +const TestServiceAccountKeyFile = "./test_data/fake-sa-key.json" + +func TestConfigPrepare(t *testing.T) { + tf, err := ioutil.TempFile("", "packer") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.Remove(tf.Name()) + tf.Close() + + cases := []struct { + Key string + Value interface{} + Err bool + }{ + { + "unknown_key", + "bad", + true, + }, + + { + "service_account_key_file", + "/tmp/i/should/not/exist", + true, + }, + + { + "service_account_key_file", + tf.Name(), + true, + }, + + { + "service_account_key_file", + TestServiceAccountKeyFile, + false, + }, + + { + "folder_id", + nil, + true, + }, + { + "folder_id", + "foo", + false, + }, + + { + "source_image_id", + nil, + true, + }, + { + "source_image_id", + "foo", + false, + }, + + { + "source_image_family", + nil, + false, + }, + { + "source_image_family", + "foo", + false, + }, + + { + "zone", + nil, + true, + }, + { + "zone", + "foo", + false, + }, + + { + "ssh_timeout", + "SO BAD", + true, + }, + { + "ssh_timeout", + "5s", + false, + }, + + { + "step_timeout", + "SO BAD", + true, + }, + { + "step_timeout", + "5s", + false, + }, + { + "image_family", + nil, + false, + }, + { + "image_family", + "", + false, + }, + { + "image_family", + "foo-bar", + false, + }, + { + "image_family", + "foo bar", + true, + }, + } + + for _, tc := range cases { + raw := testConfig(t) + + if tc.Value == nil { + delete(raw, tc.Key) + } else { + raw[tc.Key] = tc.Value + } + + if tc.Key == "service_account_key_file" { + delete(raw, "token") + } + + _, warns, errs := NewConfig(raw) + + if tc.Err { + testConfigErr(t, warns, errs, tc.Key) + } else { + testConfigOk(t, warns, errs) + } + } +} + +func TestConfigDefaults(t *testing.T) { + cases := []struct { + Read func(c *Config) interface{} + Value interface{} + }{ + { + func(c *Config) interface{} { return c.Communicator.Type }, + "ssh", + }, + + { + func(c *Config) interface{} { return c.Communicator.SSHPort }, + 22, + }, + } + + for _, tc := range cases { + raw := testConfig(t) + + c, warns, errs := NewConfig(raw) + testConfigOk(t, warns, errs) + + actual := tc.Read(c) + if actual != tc.Value { + t.Fatalf("bad: %#v", actual) + } + } +} + +func TestImageName(t *testing.T) { + raw := testConfig(t) + + c, _, _ := NewConfig(raw) + if !strings.HasPrefix(c.ImageName, "packer-") { + t.Fatalf("ImageName should have 'packer-' prefix, found %s", c.ImageName) + } + if strings.Contains(c.ImageName, "{{timestamp}}") { + t.Errorf("ImageName should be interpolated; found %s", c.ImageName) + } +} + +func TestZone(t *testing.T) { + raw := testConfig(t) + + c, _, _ := NewConfig(raw) + if c.Zone != "ru-central1-a" { + t.Fatalf("Zone should be 'ru-central1-a' given, but is '%s'", c.Zone) + } +} + +// Helper stuff below + +func testConfig(t *testing.T) (config map[string]interface{}) { + + config = map[string]interface{}{ + "token": "test_token", + "folder_id": "hashicorp", + "source_image_id": "foo", + "ssh_username": "root", + "image_family": "bar", + "image_product_ids": []string{ + "test-license", + }, + "zone": "ru-central1-a", + } + + return config +} + +func testConfigErr(t *testing.T, warns []string, err error, extra string) { + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err == nil { + t.Fatalf("should error: %s", extra) + } +} + +func testConfigOk(t *testing.T, warns []string, err error) { + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err != nil { + t.Fatalf("bad: %s", err) + } +} diff --git a/builder/yandex/driver_yc.go b/builder/yandex/driver_yc.go new file mode 100644 index 000000000..3bd40a45a --- /dev/null +++ b/builder/yandex/driver_yc.go @@ -0,0 +1,124 @@ +package yandex + +import ( + "context" + "log" + + "github.com/hashicorp/packer/helper/useragent" + "github.com/hashicorp/packer/packer" + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" + + "github.com/yandex-cloud/go-genproto/yandex/cloud/compute/v1" + "github.com/yandex-cloud/go-genproto/yandex/cloud/endpoint" + ycsdk "github.com/yandex-cloud/go-sdk" + "github.com/yandex-cloud/go-sdk/iamkey" + "github.com/yandex-cloud/go-sdk/pkg/requestid" +) + +type Driver interface { + DeleteImage(id string) error + SDK() *ycsdk.SDK + GetImage(imageID string) (*Image, error) + GetImageFromFolder(folderID string, family string) (*Image, error) +} + +type driverYC struct { + sdk *ycsdk.SDK + ui packer.Ui +} + +func (d *driverYC) GetImage(imageID string) (*Image, error) { + image, err := d.sdk.Compute().Image().Get(context.Background(), &compute.GetImageRequest{ + ImageId: imageID, + }) + if err != nil { + return nil, err + } + + return &Image{ + ID: image.Id, + Labels: image.Labels, + Licenses: image.ProductIds, + Name: image.Name, + FolderID: image.FolderId, + MinDiskSizeGb: toGigabytes(image.MinDiskSize), + SizeGb: toGigabytes(image.StorageSize), + }, nil +} + +func (d *driverYC) GetImageFromFolder(folderID string, family string) (*Image, error) { + image, err := d.sdk.Compute().Image().GetLatestByFamily(context.Background(), &compute.GetImageLatestByFamilyRequest{ + FolderId: folderID, + Family: family, + }) + if err != nil { + return nil, err + } + + return &Image{ + ID: image.Id, + Labels: image.Labels, + Licenses: image.ProductIds, + Name: image.Name, + FolderID: image.FolderId, + MinDiskSizeGb: toGigabytes(image.MinDiskSize), + SizeGb: toGigabytes(image.StorageSize), + }, nil +} + +func NewDriverYandexCloud(ui packer.Ui, config *Config) (Driver, error) { + log.Printf("[INFO] Initialize Yandex Cloud client...") + + sdkConfig := ycsdk.Config{} + + if config.Endpoint != "" { + sdkConfig.Endpoint = config.Endpoint + } + + switch { + case config.Token != "": + sdkConfig.Credentials = ycsdk.OAuthToken(config.Token) + + case config.ServiceAccountKeyFile != "": + key, err := iamkey.ReadFromJSONFile(config.ServiceAccountKeyFile) + if err != nil { + return nil, err + } + + credentials, err := ycsdk.ServiceAccountKey(key) + if err != nil { + return nil, err + } + + sdkConfig.Credentials = credentials + } + + userAgentMD := metadata.Pairs("user-agent", useragent.String()) + + sdk, err := ycsdk.Build(context.Background(), sdkConfig, + grpc.WithDefaultCallOptions(grpc.Header(&userAgentMD)), + grpc.WithUnaryInterceptor(requestid.Interceptor())) + + if err != nil { + return nil, err + } + + if _, err = sdk.ApiEndpoint().ApiEndpoint().List(context.Background(), &endpoint.ListApiEndpointsRequest{}); err != nil { + return nil, err + } + + return &driverYC{ + sdk: sdk, + ui: ui, + }, nil + +} + +func (d *driverYC) DeleteImage(ID string) error { + return nil +} + +func (d *driverYC) SDK() *ycsdk.SDK { + return d.sdk +} diff --git a/builder/yandex/image.go b/builder/yandex/image.go new file mode 100644 index 000000000..e52a3b6b8 --- /dev/null +++ b/builder/yandex/image.go @@ -0,0 +1,11 @@ +package yandex + +type Image struct { + ID string + FolderID string + Labels map[string]string + Licenses []string + MinDiskSizeGb int + Name string + SizeGb int +} diff --git a/builder/yandex/ssh.go b/builder/yandex/ssh.go new file mode 100644 index 000000000..c32f16b53 --- /dev/null +++ b/builder/yandex/ssh.go @@ -0,0 +1,10 @@ +package yandex + +import ( + "github.com/hashicorp/packer/helper/multistep" +) + +func commHost(state multistep.StateBag) (string, error) { + ipAddress := state.Get("instance_ip").(string) + return ipAddress, nil +} diff --git a/builder/yandex/step_create_image.go b/builder/yandex/step_create_image.go new file mode 100644 index 000000000..9d764c30b --- /dev/null +++ b/builder/yandex/step_create_image.go @@ -0,0 +1,70 @@ +package yandex + +import ( + "context" + "errors" + "fmt" + "log" + + "github.com/hashicorp/packer/helper/multistep" + "github.com/hashicorp/packer/packer" + + "github.com/yandex-cloud/go-genproto/yandex/cloud/compute/v1" + ycsdk "github.com/yandex-cloud/go-sdk" +) + +type stepCreateImage struct{} + +func (stepCreateImage) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { + sdk := state.Get("sdk").(*ycsdk.SDK) + ui := state.Get("ui").(packer.Ui) + c := state.Get("config").(*Config) + diskID := state.Get("disk_id").(string) + + ui.Say(fmt.Sprintf("Creating image: %v", c.ImageName)) + op, err := sdk.WrapOperation(sdk.Compute().Image().Create(context.Background(), &compute.CreateImageRequest{ + FolderId: c.FolderID, + Name: c.ImageName, + Family: c.ImageFamily, + Description: c.ImageDescription, + Labels: c.ImageLabels, + ProductIds: c.ImageProductIDs, + Source: &compute.CreateImageRequest_DiskId{ + DiskId: diskID, + }, + })) + if err != nil { + return stepHaltWithError(state, fmt.Errorf("Error creating image: %s", err)) + } + + // With the pending state over, verify that we're in the active state + ui.Say("Waiting for image to complete...") + if err := op.Wait(context.Background()); err != nil { + return stepHaltWithError(state, fmt.Errorf("Error waiting for image: %s", err)) + } + + resp, err := op.Response() + if err != nil { + return stepHaltWithError(state, err) + } + + image, ok := resp.(*compute.Image) + if !ok { + return stepHaltWithError(state, errors.New("Response doesn't contain Image")) + } + + log.Printf("Image ID: %s", image.Id) + log.Printf("Image Name: %s", image.Name) + log.Printf("Image Family: %s", image.Family) + log.Printf("Image Description: %s", image.Description) + log.Printf("Image Storage size: %d", image.StorageSize) + state.Put("image_id", image.Id) + state.Put("image_name", c.ImageName) + state.Put("image_family", c.ImageFamily) + + return multistep.ActionContinue +} + +func (stepCreateImage) Cleanup(state multistep.StateBag) { + // no cleanup +} diff --git a/builder/yandex/step_create_instance.go b/builder/yandex/step_create_instance.go new file mode 100644 index 000000000..fa95bb3ad --- /dev/null +++ b/builder/yandex/step_create_instance.go @@ -0,0 +1,337 @@ +package yandex + +import ( + "context" + "errors" + "fmt" + "io/ioutil" + "time" + + "github.com/c2h5oh/datasize" + "github.com/hashicorp/packer/common/uuid" + "github.com/hashicorp/packer/helper/multistep" + "github.com/hashicorp/packer/packer" + + "github.com/yandex-cloud/go-genproto/yandex/cloud/compute/v1" + "github.com/yandex-cloud/go-genproto/yandex/cloud/vpc/v1" + ycsdk "github.com/yandex-cloud/go-sdk" +) + +type stepCreateInstance struct { + Debug bool + SerialLogFile string + cleanupInstanceID string + cleanupNetworkID string + cleanupSubnetID string +} + +func createNetwork(c *Config, d Driver) (*vpc.Network, error) { + req := &vpc.CreateNetworkRequest{ + FolderId: c.FolderID, + Name: fmt.Sprintf("packer-network-%s", uuid.TimeOrderedUUID()), + } + + ctx := context.Background() + sdk := d.SDK() + + op, err := sdk.WrapOperation(sdk.VPC().Network().Create(ctx, req)) + if err != nil { + return nil, err + } + + err = op.Wait(ctx) + if err != nil { + return nil, err + } + + resp, err := op.Response() + if err != nil { + return nil, err + } + + network, ok := resp.(*vpc.Network) + if !ok { + return nil, errors.New("network create operation response doesn't contain Network") + } + return network, nil +} + +func createSubnet(c *Config, d Driver, networkID string) (*vpc.Subnet, error) { + req := &vpc.CreateSubnetRequest{ + FolderId: c.FolderID, + NetworkId: networkID, + Name: fmt.Sprintf("packer-subnet-%s", uuid.TimeOrderedUUID()), + ZoneId: c.Zone, + V4CidrBlocks: []string{"192.168.111.0/24"}, + } + + ctx := context.Background() + sdk := d.SDK() + + op, err := sdk.WrapOperation(sdk.VPC().Subnet().Create(ctx, req)) + if err != nil { + return nil, err + } + + err = op.Wait(ctx) + if err != nil { + return nil, err + } + + resp, err := op.Response() + if err != nil { + return nil, err + } + + network, ok := resp.(*vpc.Subnet) + if !ok { + return nil, errors.New("subnet create operation response doesn't contain Network") + } + return network, nil +} + +func getImage(c *Config, d Driver) (*Image, error) { + if c.SourceImageID != "" { + return d.GetImage(c.SourceImageID) + } + + familyName := c.SourceImageFamily + if c.SourceImageFolderID != "" { + return d.GetImageFromFolder(c.SourceImageFolderID, familyName) + } + return d.GetImageFromFolder("standard-images", familyName) +} + +func (s *stepCreateInstance) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { + sdk := state.Get("sdk").(*ycsdk.SDK) + ui := state.Get("ui").(packer.Ui) + c := state.Get("config").(*Config) + d := state.Get("driver").(Driver) + + // create or reuse Subnet + instanceSubnetID := "" + if c.SubnetID == "" { + // create Network + ui.Say("Creating network...") + network, err := createNetwork(c, d) + if err != nil { + return stepHaltWithError(state, fmt.Errorf("Error creating network: %s", err)) + } + state.Put("network_id", network.Id) + s.cleanupNetworkID = network.Id + + ui.Say("Creating subnet in zone...") + subnet, err := createSubnet(c, d, network.Id) + if err != nil { + return stepHaltWithError(state, fmt.Errorf("Error creating subnet: %s", err)) + } + state.Put("subnet_id", subnet.Id) + instanceSubnetID = subnet.Id + // save for cleanup + s.cleanupSubnetID = subnet.Id + } else { + ui.Say("Use provided subnet id " + c.SubnetID) + instanceSubnetID = c.SubnetID + } + + // Create an instance based on the configuration + ui.Say("Creating instance...") + sourceImage, err := getImage(c, d) + if err != nil { + return stepHaltWithError(state, fmt.Errorf("Error getting source image for instance creation: %s", err)) + } + + if sourceImage.MinDiskSizeGb > c.DiskSizeGb { + return stepHaltWithError(state, fmt.Errorf("Instance DiskSizeGb (%d) should be equal or greater "+ + "than SourceImage disk requirement (%d)", c.DiskSizeGb, sourceImage.MinDiskSizeGb)) + } + + instanceMetadata, err := c.createInstanceMetadata(string(c.Communicator.SSHPublicKey)) + if err != nil { + return stepHaltWithError(state, fmt.Errorf("instance metadata prepare error: %s", err)) + } + + // TODO make part metadata prepare process + if c.UseIPv6 { + // this ugly hack will replace user provided 'user-data' + userData := `#cloud-config +runcmd: +- [ sh, -c, '/sbin/dhclient -6 -D LL -nw -pf /run/dhclient_ipv6.eth0.pid -lf /var/lib/dhcp/dhclient_ipv6.eth0.leases eth0' ] +` + instanceMetadata["user-data"] = userData + } + + req := &compute.CreateInstanceRequest{ + FolderId: c.FolderID, + Name: c.InstanceName, + Labels: c.Labels, + ZoneId: c.Zone, + PlatformId: "standard-v1", + ResourcesSpec: &compute.ResourcesSpec{ + Memory: toBytes(c.InstanceMemory), + Cores: int64(c.InstanceCores), + }, + Metadata: instanceMetadata, + BootDiskSpec: &compute.AttachedDiskSpec{ + AutoDelete: true, + Disk: &compute.AttachedDiskSpec_DiskSpec_{ + DiskSpec: &compute.AttachedDiskSpec_DiskSpec{ + Name: c.DiskName, + TypeId: c.DiskType, + Size: int64((datasize.ByteSize(c.DiskSizeGb) * datasize.GB).Bytes()), + Source: &compute.AttachedDiskSpec_DiskSpec_ImageId{ + ImageId: sourceImage.ID, + }, + }, + }, + }, + NetworkInterfaceSpecs: []*compute.NetworkInterfaceSpec{ + { + SubnetId: instanceSubnetID, + PrimaryV4AddressSpec: &compute.PrimaryAddressSpec{}, + }, + }, + } + + if c.UseIPv6 { + req.NetworkInterfaceSpecs[0].PrimaryV6AddressSpec = &compute.PrimaryAddressSpec{} + } + + if c.UseIPv4Nat { + req.NetworkInterfaceSpecs[0].PrimaryV4AddressSpec = &compute.PrimaryAddressSpec{ + OneToOneNatSpec: &compute.OneToOneNatSpec{ + IpVersion: compute.IpVersion_IPV4, + }, + } + } + + ctx := context.Background() + op, err := sdk.WrapOperation(sdk.Compute().Instance().Create(ctx, req)) + if err != nil { + return stepHaltWithError(state, fmt.Errorf("Error create instance: %s", err)) + } + + err = op.Wait(ctx) + if err != nil { + return stepHaltWithError(state, fmt.Errorf("Error create instance: %s", err)) + } + + resp, err := op.Response() + if err != nil { + return stepHaltWithError(state, err) + } + + instance, ok := resp.(*compute.Instance) + if !ok { + return stepHaltWithError(state, fmt.Errorf("response doesn't contain Instance")) + } + + // We use this in cleanup + s.cleanupInstanceID = instance.Id + + if s.Debug { + ui.Message(fmt.Sprintf("Instance ID %s started. Current status %s", instance.Id, instance.Status)) + } + + // Store the instance id for later + state.Put("instance_id", instance.Id) + state.Put("disk_id", instance.BootDisk.DiskId) + + return multistep.ActionContinue +} + +func (s *stepCreateInstance) Cleanup(state multistep.StateBag) { + // If the cleanupInstanceID isn't there, we probably never created it + if s.cleanupInstanceID == "" { + return + } + + sdk := state.Get("sdk").(*ycsdk.SDK) + ui := state.Get("ui").(packer.Ui) + + if s.SerialLogFile != "" { + ui.Say("Current state 'cancelled' or 'halted'...") + err := s.writeSerialLogFile(state) + if err != nil { + ui.Error(err.Error()) + } + } + + // Destroy the instance we just created + ui.Say("Destroying instance...") + + _, err := sdk.Compute().Instance().Delete(context.Background(), &compute.DeleteInstanceRequest{ + InstanceId: s.cleanupInstanceID, + }) + if err != nil { + ui.Error(fmt.Sprintf( + "Error destroying instance (id: %s): %s.\nPlease destroy it manually", s.cleanupInstanceID, err)) + } + + if s.cleanupSubnetID != "" { + // some sleep before delete network components + time.Sleep(30 * time.Second) + + // Destroy the subnet we just created + ui.Say("Destroying subnet...") + _, err = sdk.VPC().Subnet().Delete(context.Background(), &vpc.DeleteSubnetRequest{ + SubnetId: s.cleanupSubnetID, + }) + if err != nil { + ui.Error(fmt.Sprintf( + "Error destroying subnet (id: %s). Please destroy it manually: %s", s.cleanupSubnetID, err)) + } + + // some sleep before delete network + time.Sleep(10 * time.Second) + + // Destroy the network we just created + ui.Say("Destroying network...") + _, err = sdk.VPC().Network().Delete(context.Background(), &vpc.DeleteNetworkRequest{ + NetworkId: s.cleanupNetworkID, + }) + if err != nil { + ui.Error(fmt.Sprintf( + "Error destroying network (id: %s). Please destroy it manually: %s", s.cleanupNetworkID, err)) + } + } + +} +func (s *stepCreateInstance) writeSerialLogFile(state multistep.StateBag) error { + sdk := state.Get("sdk").(*ycsdk.SDK) + ui := state.Get("ui").(packer.Ui) + + ui.Say("Try get serial port output to file " + s.SerialLogFile) + serialOutput, err := sdk.Compute().Instance().GetSerialPortOutput(context.Background(), &compute.GetInstanceSerialPortOutputRequest{ + InstanceId: s.cleanupInstanceID, + }) + if err != nil { + return fmt.Errorf("Failed to get serial port output for instance (id: %s): %s", s.cleanupInstanceID, err) + } + if err := ioutil.WriteFile(s.SerialLogFile, []byte(serialOutput.Contents), 0600); err != nil { + return fmt.Errorf("Failed to write serial port output to file: %s", err) + } + ui.Message("Serial port output has been successfully written") + return nil +} + +func (c *Config) createInstanceMetadata(sshPublicKey string) (map[string]string, error) { + instanceMetadata := make(map[string]string) + var err error + + // Copy metadata from config. + for k, v := range c.Metadata { + instanceMetadata[k] = v + } + + if sshPublicKey != "" { + sshMetaKey := "ssh-keys" + sshKeys := fmt.Sprintf("%s:%s", c.Communicator.SSHUsername, sshPublicKey) + if confSSHKeys, exists := instanceMetadata[sshMetaKey]; exists { + sshKeys = fmt.Sprintf("%s\n%s", sshKeys, confSSHKeys) + } + instanceMetadata[sshMetaKey] = sshKeys + } + + return instanceMetadata, err +} diff --git a/builder/yandex/step_create_ssh_key.go b/builder/yandex/step_create_ssh_key.go new file mode 100644 index 000000000..2bc71c739 --- /dev/null +++ b/builder/yandex/step_create_ssh_key.go @@ -0,0 +1,102 @@ +package yandex + +import ( + "context" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "fmt" + "io/ioutil" + "log" + + "github.com/hashicorp/packer/common/uuid" + "github.com/hashicorp/packer/helper/multistep" + "github.com/hashicorp/packer/packer" + "golang.org/x/crypto/ssh" +) + +type stepCreateSSHKey struct { + Debug bool + DebugKeyPath string +} + +func (s *stepCreateSSHKey) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { + ui := state.Get("ui").(packer.Ui) + c := state.Get("config").(*Config) + + if c.Communicator.SSHPrivateKeyFile != "" { + ui.Say("Using existing SSH private key") + privateKeyBytes, err := c.Communicator.ReadSSHPrivateKeyFile() + if err != nil { + state.Put("error", err) + return multistep.ActionHalt + } + + key, err := ssh.ParsePrivateKey(privateKeyBytes) + if err != nil { + err = fmt.Errorf("Error parsing 'ssh_private_key_file': %s", err) + ui.Error(err.Error()) + state.Put("error", err) + return multistep.ActionHalt + } + + c.Communicator.SSHPublicKey = ssh.MarshalAuthorizedKey(key.PublicKey()) + c.Communicator.SSHPrivateKey = privateKeyBytes + + return multistep.ActionContinue + } + + ui.Say("Creating temporary ssh key for instance...") + + priv, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return stepHaltWithError(state, fmt.Errorf("Error generating temporary SSH key: %s", err)) + } + + // ASN.1 DER encoded form + privDer := x509.MarshalPKCS1PrivateKey(priv) + privBlk := pem.Block{ + Type: "RSA PRIVATE KEY", + Headers: nil, + Bytes: privDer, + } + + // Marshal the public key into SSH compatible format + // TODO properly handle the public key error + pub, _ := ssh.NewPublicKey(&priv.PublicKey) + pubSSHFormat := string(ssh.MarshalAuthorizedKey(pub)) + + // The name of the public key on DO + name := fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID()) + + hashMd5 := ssh.FingerprintLegacyMD5(pub) + hashSha256 := ssh.FingerprintSHA256(pub) + + log.Printf("[INFO] temporary ssh key name: %s", name) + log.Printf("[INFO] md5 hash of ssh pub key: %s", hashMd5) + log.Printf("[INFO] sha256 hash of ssh pub key: %s", hashSha256) + + // Remember some state for the future + //state.Put("ssh_key_id", key.ID) + state.Put("ssh_key_public", pubSSHFormat) + state.Put("ssh_key_name", name) + + // Set the private key in the config for later + c.Communicator.SSHPrivateKey = pem.EncodeToMemory(&privBlk) + c.Communicator.SSHPublicKey = ssh.MarshalAuthorizedKey(pub) + + // If we're in debug mode, output the private key to the working directory. + if s.Debug { + ui.Message(fmt.Sprintf("Saving key for debug purposes: %s", s.DebugKeyPath)) + err := ioutil.WriteFile(s.DebugKeyPath, c.Communicator.SSHPrivateKey, 0600) + if err != nil { + return stepHaltWithError(state, fmt.Errorf("Error saving debug key: %s", err)) + } + } + + return multistep.ActionContinue +} + +func (s *stepCreateSSHKey) Cleanup(state multistep.StateBag) { +} diff --git a/builder/yandex/step_instance_info.go b/builder/yandex/step_instance_info.go new file mode 100644 index 000000000..bd6f192c6 --- /dev/null +++ b/builder/yandex/step_instance_info.go @@ -0,0 +1,109 @@ +package yandex + +import ( + "context" + "errors" + "fmt" + + "github.com/hashicorp/packer/helper/multistep" + "github.com/hashicorp/packer/packer" + + "github.com/yandex-cloud/go-genproto/yandex/cloud/compute/v1" + ycsdk "github.com/yandex-cloud/go-sdk" +) + +type stepInstanceInfo struct{} + +func (s *stepInstanceInfo) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { + sdk := state.Get("sdk").(*ycsdk.SDK) + ui := state.Get("ui").(packer.Ui) + c := state.Get("config").(*Config) + instanceID := state.Get("instance_id").(string) + + ui.Say("Waiting for instance to become active...") + + // Set the IP on the state for later + instance, err := sdk.Compute().Instance().Get(context.Background(), &compute.GetInstanceRequest{ + InstanceId: instanceID, + View: compute.InstanceView_FULL, + }) + if err != nil { + return stepHaltWithError(state, fmt.Errorf("Error retrieving instance data: %s", err)) + } + + instanceIP, err := getInstanceIPAddress(c, instance) + if err != nil { + return stepHaltWithError(state, fmt.Errorf("Failed to find instance ip address: %s", err)) + } + + state.Put("instance_ip", instanceIP) + ui.Message(fmt.Sprintf("Detected instance IP: %s", instanceIP)) + + return multistep.ActionContinue +} + +func getInstanceIPAddress(c *Config, instance *compute.Instance) (address string, err error) { + // Instance could have several network interfaces with different configuration each + // Get all possible addresses for instance + addrIPV4Internal, addrIPV4External, addrIPV6Addr, err := instanceAddresses(instance) + if err != nil { + return "", err + } + + if c.UseIPv6 { + if addrIPV6Addr != "" { + return "[" + addrIPV6Addr + "]", nil + } + return "", errors.New("instance has no one IPv6 address") + } + + if c.UseInternalIP { + if addrIPV4Internal != "" { + return addrIPV4Internal, nil + } + return "", errors.New("instance has no one IPv4 internal address") + } + if addrIPV4External != "" { + return addrIPV4External, nil + } + return "", errors.New("instance has no one IPv4 external address") +} + +func instanceAddresses(instance *compute.Instance) (ipV4Int, ipV4Ext, ipV6 string, err error) { + if len(instance.NetworkInterfaces) == 0 { + return "", "", "", errors.New("No one network interface found for an instance") + } + + var ipV4IntFound, ipV4ExtFound, ipV6Found bool + for _, iface := range instance.NetworkInterfaces { + if !ipV6Found && iface.PrimaryV6Address != nil { + ipV6 = iface.PrimaryV6Address.Address + ipV6Found = true + } + + if !ipV4IntFound && iface.PrimaryV4Address != nil { + ipV4Int = iface.PrimaryV4Address.Address + ipV4IntFound = true + + if !ipV4ExtFound && iface.PrimaryV4Address.OneToOneNat != nil { + ipV4Ext = iface.PrimaryV4Address.OneToOneNat.Address + ipV4ExtFound = true + } + } + + if ipV6Found && ipV4IntFound && ipV4ExtFound { + break + } + } + + if !ipV4IntFound { + // internal ipV4 address always should present + return "", "", "", errors.New("No IPv4 internal address found. Bug?") + } + + return +} + +func (s *stepInstanceInfo) Cleanup(state multistep.StateBag) { + // no cleanup +} diff --git a/builder/yandex/step_shutdown.go b/builder/yandex/step_shutdown.go new file mode 100644 index 000000000..e1f189bd3 --- /dev/null +++ b/builder/yandex/step_shutdown.go @@ -0,0 +1,49 @@ +package yandex + +import ( + "context" + "fmt" + + "github.com/hashicorp/packer/helper/multistep" + "github.com/hashicorp/packer/packer" + + "github.com/yandex-cloud/go-genproto/yandex/cloud/compute/v1" + ycsdk "github.com/yandex-cloud/go-sdk" +) + +type stepShutdown struct { + Debug bool +} + +func (s *stepShutdown) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { + sdk := state.Get("sdk").(*ycsdk.SDK) + ui := state.Get("ui").(packer.Ui) + instanceID := state.Get("instance_id").(string) + + // Gracefully power off the instance. We have to retry this a number + // of times because sometimes it says it completed when it actually + // did absolutely nothing (*ALAKAZAM!* magic!). We give up after + // a pretty arbitrary amount of time. + ui.Say("Gracefully shutting down instance...") + op, err := sdk.WrapOperation(sdk.Compute().Instance().Stop(context.Background(), &compute.StopInstanceRequest{ + InstanceId: instanceID, + })) + if err != nil { + return stepHaltWithError(state, fmt.Errorf("Error shutting down instance: %s", err)) + } + err = op.Wait(context.Background()) + if err != nil { + return stepHaltWithError(state, fmt.Errorf("Error shutting down instance: %s", err)) + } + + if s.Debug { + ui.Message("Instance status before image create:") + displayInstanceStatus(sdk, instanceID, ui) + } + + return multistep.ActionContinue +} + +func (s *stepShutdown) Cleanup(state multistep.StateBag) { + // no cleanup +} diff --git a/builder/yandex/template_func.go b/builder/yandex/template_func.go new file mode 100644 index 000000000..91f83a9d5 --- /dev/null +++ b/builder/yandex/template_func.go @@ -0,0 +1,36 @@ +package yandex + +import "strings" +import "text/template" + +func isalphanumeric(b byte) bool { + if '0' <= b && b <= '9' { + return true + } + if 'a' <= b && b <= 'z' { + return true + } + return false +} + +// Clean up image name by replacing invalid characters with "-" +// and converting upper cases to lower cases +func templateCleanImageName(s string) string { + if reImageFamily.MatchString(s) { + return s + } + b := []byte(strings.ToLower(s)) + newb := make([]byte, len(b)) + for i := range newb { + if isalphanumeric(b[i]) { + newb[i] = b[i] + } else { + newb[i] = '-' + } + } + return string(newb) +} + +var TemplateFuncs = template.FuncMap{ + "clean_image_name": templateCleanImageName, +} diff --git a/builder/yandex/test_data/fake-sa-key.json b/builder/yandex/test_data/fake-sa-key.json new file mode 100644 index 000000000..df79c0126 --- /dev/null +++ b/builder/yandex/test_data/fake-sa-key.json @@ -0,0 +1,9 @@ +{ + "id": "ajeboa0du6edu6m43c3t", + "service_account_id": "ajeq7dsmihqple6761c5", + "created_at": "2018-11-19T13:38:09Z", + "description": "description", + "key_algorithm": "RSA_4096", + "public_key": "-----BEGIN PUBLIC KEY-----\nMIICCgKCAgEAo/s1lN5vFpFNJvS/l+yRilQHAPDeC3JqBwpLstbqJXW4kAUaKKoe\nxkIuJuPUKOUcd/JE3LXOEt/LOFb9mkCRdpjaIW7Jd5Fw0kTHIZ5rDoq7DZx0LV9b\nGJNskdccd6M6stb1GEqVuGpVcyXMCH8tMSG3c85DkcAg0cxXgyrirAzHMPiWSTpj\nJjICkxXRVj01Xq7dIDqL2LSMrZ2kLda5m+CnfscUbwnGRPPoEg20jLiEgBM2o43e\nhpWko1NStRR5fMQcQSUBbdtvbfPracjZz2/fq4fZfqlnObgq3WpYpdGynniLH3i5\nbxPM3ufYL3HY2w5aIOY6KIwMKLf3WYlug90ieviMYAvCukrCASwyqBQlt3MKCHlN\nIcebZXJDQ1VSBuEs+4qXYlhG1p+5C07zahzigNNTm6rEo47FFfClF04mv2uJN42F\nfWlEPR+V9JHBcfcBCdvyhiGzftl/vDo2NdO751ETIhyNKzxM/Ve2PR9h/qcuEatC\nLlXUA+40epNNHbSxAauxcngyrtkn7FZAEhdjyTtx46sELyb90Z56WgnbNUUGnsS/\nHBnBy5z8RyCmI5MjTC2NtplVqtAWkG+x59mU3GoCeuI8EaNtu2YPXhl1ovRkS4NB\n1G0F4c5FiJ27/E2MbNKlV5iw9ICcDforATYTeqiXbkkEKqIIiZYZWOsCAwEAAQ==\n-----END PUBLIC KEY-----\n", + "private_key": "-----BEGIN PRIVATE KEY-----\n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\n-----END PRIVATE KEY-----\n" +} \ No newline at end of file diff --git a/builder/yandex/util.go b/builder/yandex/util.go new file mode 100644 index 000000000..52eb63f90 --- /dev/null +++ b/builder/yandex/util.go @@ -0,0 +1,38 @@ +package yandex + +import ( + "context" + "fmt" + + "github.com/c2h5oh/datasize" + "github.com/hashicorp/packer/helper/multistep" + "github.com/hashicorp/packer/packer" + + "github.com/yandex-cloud/go-genproto/yandex/cloud/compute/v1" + ycsdk "github.com/yandex-cloud/go-sdk" +) + +func stepHaltWithError(state multistep.StateBag, err error) multistep.StepAction { + ui := state.Get("ui").(packer.Ui) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt +} + +func displayInstanceStatus(sdk *ycsdk.SDK, instanceID string, ui packer.Ui) { + instance, err := sdk.Compute().Instance().Get(context.Background(), &compute.GetInstanceRequest{ + InstanceId: instanceID, + }) + if err != nil { + ui.Error(fmt.Sprintf("Fail to get instance data: %s", err)) + } + ui.Message(fmt.Sprintf("Current instance status %s", instance.Status)) +} + +func toGigabytes(bytesCount int64) int { + return int((datasize.ByteSize(bytesCount) * datasize.B).GBytes()) +} + +func toBytes(gigabytesCount int) int64 { + return int64((datasize.ByteSize(gigabytesCount) * datasize.GB).Bytes()) +}