From e184fa2726d9d179cc145727e41729af0c12832b Mon Sep 17 00:00:00 2001 From: Christophe Jauffret Date: Mon, 13 Feb 2023 18:35:42 +0100 Subject: [PATCH] implement category mapping for image implement common checkTask validation cleaning messaging --- builder/nutanix/config.go | 13 ++++ builder/nutanix/config.hcl2spec.go | 4 + builder/nutanix/driver.go | 111 +++++++++++++++------------- builder/nutanix/step_build_vm.go | 11 ++- builder/nutanix/step_copy_image.go | 11 ++- builder/nutanix/step_shutdown_vm.go | 4 +- 6 files changed, 91 insertions(+), 63 deletions(-) diff --git a/builder/nutanix/config.go b/builder/nutanix/config.go index e88d6f7..efd6ec7 100644 --- a/builder/nutanix/config.go +++ b/builder/nutanix/config.go @@ -36,6 +36,8 @@ type Config struct { VmConfig `mapstructure:",squash"` ForceDeregister bool `mapstructure:"force_deregister" json:"force_deregister" required:"false"` ImageDescription string `mapstructure:"image_description" json:"image_description" required:"false"` + ImageCategoryKey string `mapstructure:"image_category_key" json:"image_category_key" required:"false"` + ImageCategoryValue string `mapstructure:"image_category_value" json:"image_category_value" required:"false"` WaitTimeout time.Duration `mapstructure:"ip_wait_timeout" json:"ip_wait_timeout" required:"false"` ctx interpolate.Context @@ -150,6 +152,17 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) { c.VmConfig.ImageName = c.VmConfig.VMName } + // Validate if both Image Category key and value are given in same time + if c.ImageCategoryKey != "" && c.ImageCategoryValue == "" { + log.Println("Nutanix Image Category value missing from configuration") + errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("missing image_category_value")) + } + + if c.ImageCategoryKey == "" && c.ImageCategoryValue != "" { + log.Println("Nutanix Image Category key missing from configuration") + errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("missing image_category_key")) + } + if c.CommConfig.SSHPort == 0 { log.Println("SSHPort not set, defaulting to 22") c.CommConfig.SSHPort = 22 diff --git a/builder/nutanix/config.hcl2spec.go b/builder/nutanix/config.hcl2spec.go index ee94afb..2c9da4d 100644 --- a/builder/nutanix/config.hcl2spec.go +++ b/builder/nutanix/config.hcl2spec.go @@ -121,6 +121,8 @@ type FlatConfig struct { UserData *string `mapstructure:"user_data" json:"user_data" required:"false" cty:"user_data" hcl:"user_data"` ForceDeregister *bool `mapstructure:"force_deregister" json:"force_deregister" required:"false" cty:"force_deregister" hcl:"force_deregister"` ImageDescription *string `mapstructure:"image_description" json:"image_description" required:"false" cty:"image_description" hcl:"image_description"` + ImageCategoryKey *string `mapstructure:"image_category_key" json:"image_category_key" required:"false" cty:"image_category_key" hcl:"image_category_key"` + ImageCategoryValue *string `mapstructure:"image_category_value" json:"image_category_value" required:"false" cty:"image_category_value" hcl:"image_category_value"` WaitTimeout *string `mapstructure:"ip_wait_timeout" json:"ip_wait_timeout" required:"false" cty:"ip_wait_timeout" hcl:"ip_wait_timeout"` } @@ -216,6 +218,8 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec { "user_data": &hcldec.AttrSpec{Name: "user_data", Type: cty.String, Required: false}, "force_deregister": &hcldec.AttrSpec{Name: "force_deregister", Type: cty.Bool, Required: false}, "image_description": &hcldec.AttrSpec{Name: "image_description", Type: cty.String, Required: false}, + "image_category_key": &hcldec.AttrSpec{Name: "image_category_key", Type: cty.String, Required: false}, + "image_category_value": &hcldec.AttrSpec{Name: "image_category_value", Type: cty.String, Required: false}, "ip_wait_timeout": &hcldec.AttrSpec{Name: "ip_wait_timeout", Type: cty.String, Required: false}, } return s diff --git a/builder/nutanix/driver.go b/builder/nutanix/driver.go index 221df10..54e70e1 100644 --- a/builder/nutanix/driver.go +++ b/builder/nutanix/driver.go @@ -31,7 +31,7 @@ type Driver interface { UploadImage(string, string, string, VmConfig) (*nutanixImage, error) DeleteImage(string) error GetImage(string) (*nutanixImage, error) - SaveVMDisk(string) (*nutanixImage, error) + SaveVMDisk(string, string, string) (*nutanixImage, error) WaitForShutdown(string, <-chan struct{}) bool } @@ -171,6 +171,29 @@ func findImageByName(conn *v3.Client, name string) (*v3.ImageIntentResponse, err return findImageByUUID(conn, *found[0].Metadata.UUID) } +func checkTask(conn *v3.Client, taskUUID string) error { + + log.Printf("checking task %s...", taskUUID) + var task *v3.TasksResponse + var err error + for i := 0; i < 120; i++ { + task, err = conn.V3.GetTask(taskUUID) + if err == nil { + if *task.Status == "SUCCEEDED" { + return nil + } else if *task.Status == "FAILED" { + return fmt.Errorf(*task.ErrorDetail) + } else { + log.Printf("current task status is: " + *task.Status) + <-time.After(5 * time.Second) + } + } else { + return err + } + } + return fmt.Errorf("check task %s timeout", taskUUID) +} + func (d *nutanixInstance) Addresses() []string { var addresses []string if len(d.nutanix.Status.Resources.NicList) > 0 { @@ -422,39 +445,19 @@ func (d *NutanixDriver) Create(req *v3.VMIntentInput) (*nutanixInstance, error) resp, err := conn.V3.CreateVM(req) if err != nil { - log.Printf("Error creating vm: [%v]", err) + log.Printf("error creating vm: %s", err.Error()) return nil, err } uuid := *resp.Metadata.UUID - taskUUID := resp.Status.ExecutionContext.TaskUUID.(string) - log.Printf("waiting for vm (%s) to create: %s", uuid, taskUUID) + log.Printf("creating vm %s...", uuid) - var vm *v3.VMIntentResponse - for { - vm, err = conn.V3.GetVM(uuid) - if err == nil { - if *vm.Status.State == "COMPLETE" { - log.Printf("vm created successfully: " + *vm.Status.State) - break - } else if *vm.Status.State == "ERROR" { - var errTxt string - for i := 0; i < len(vm.Status.MessageList); i++ { - errTxt = *(vm.Status.MessageList)[i].Message - log.Printf("Nutanix Error Message: %s", *(vm.Status.MessageList)[i].Message) - log.Printf("Nutanix Error Reason: %s", *(vm.Status.MessageList)[i].Reason) - log.Printf("Nutanix Error Details: %s", (vm.Status.MessageList)[i].Details) - } - return nil, fmt.Errorf(errTxt) - } else { - log.Printf("Current status is: " + *vm.Status.State) - time.Sleep(5 * time.Second) - } - } else { - log.Printf("Error while getting VM Status, %s", err.Error()) - return nil, err - } + err = checkTask(conn, resp.Status.ExecutionContext.TaskUUID.(string)) + + if err != nil { + log.Printf("error creating vm: %s", err.Error()) + return nil, err } // Wait for the VM obtain an IP address @@ -462,7 +465,7 @@ func (d *NutanixDriver) Create(req *v3.VMIntentInput) (*nutanixInstance, error) log.Printf("[INFO] Waiting for IP, up to timeout: %s", d.Config.WaitTimeout) iteration := int(d.Config.WaitTimeout.Seconds()) / 5 - + var vm *v3.VMIntentResponse for i := 0; i < iteration; i++ { vm, err = conn.V3.GetVM(uuid) if err != nil || len(vm.Status.Resources.NicList[0].IPEndpointList) == (0) { @@ -570,7 +573,7 @@ func (d *NutanixDriver) UploadImage(imagePath string, sourceType string, imageTy if *running.Status.State == "COMPLETE" { break } - time.Sleep(5 * time.Second) + <-time.After(5 * time.Second) } if sourceType == "PATH" { @@ -586,7 +589,7 @@ func (d *NutanixDriver) UploadImage(imagePath string, sourceType string, imageTy if *running.Status.State == "COMPLETE" { break } - time.Sleep(5 * time.Second) + <-time.After(5 * time.Second) } } return &nutanixImage{image: *image}, nil @@ -727,7 +730,7 @@ func (d *NutanixDriver) PowerOff(vmUUID string) error { log.Printf("PowerOff task: %s", taskUUID) return nil } -func (d *NutanixDriver) SaveVMDisk(diskUUID string) (*nutanixImage, error) { +func (d *NutanixDriver) SaveVMDisk(diskUUID string, imageCategoryKey string, imageCategoryValue string) (*nutanixImage, error) { configCreds := client.Credentials{ URL: fmt.Sprintf("%s:%d", d.ClusterConfig.Endpoint, d.ClusterConfig.Port), @@ -751,29 +754,22 @@ func (d *NutanixDriver) SaveVMDisk(diskUUID string) (*nutanixImage, error) { return nil, fmt.Errorf("error while ListAllImage, %s", err.Error()) } if *ImageList.Metadata.TotalMatches == 0 { - log.Println("Image with given Name not found, no need to deregister") + log.Println("image with given Name not found, no need to deregister") } else if *ImageList.Metadata.TotalMatches > 1 { - log.Println("More than one image with given Name found, will not deregister") + log.Println("more than one image with given Name found, will not deregister") } else if *ImageList.Metadata.TotalMatches == 1 { - log.Println("Exactly one image with given Name found, will deregister") + log.Println("exactly one image with given Name found, will deregister") resp, err := conn.V3.DeleteImage(*ImageList.Entities[0].Metadata.UUID) if err != nil { - return nil, fmt.Errorf("error while DeleteImage, %s", err.Error()) + return nil, fmt.Errorf("error while Delete Image, %s", err.Error()) } - taskUUID := resp.Status.ExecutionContext.TaskUUID.(string) - log.Printf("Wait until delete Image %s is finished, %s\n", *ImageList.Entities[0].Metadata.UUID, taskUUID) - // Wait for the Image to be deleted - for i := 0; i < 1200; i++ { - resp, err := conn.V3.GetTask(taskUUID) - if err != nil || *resp.Status != "SUCCEEDED" { - <-time.After(1 * time.Second) - continue - } - if *resp.Status == "SUCCEEDED" { - break - } - return nil, fmt.Errorf("error while Image Delete getting Task Status, %s", err.Error()) + + log.Printf("deleting image %s...\n", *ImageList.Entities[0].Metadata.UUID) + err = checkTask(conn, resp.Status.ExecutionContext.TaskUUID.(string)) + + if err != nil { + return nil, fmt.Errorf("error while Delete Image, %s", err.Error()) } } } @@ -797,11 +793,24 @@ func (d *NutanixDriver) SaveVMDisk(diskUUID string) (*nutanixImage, error) { }, } + if imageCategoryKey != "" && imageCategoryValue != "" { + c := make(map[string]string) + c[imageCategoryKey] = imageCategoryValue + req.Metadata.Categories = c + } + image, err := conn.V3.CreateImage(req) if err != nil { - return nil, fmt.Errorf("error while CreateImage, %s", err.Error()) + return nil, fmt.Errorf("error while Create Image, %s", err.Error()) } - return &nutanixImage{image: *image}, nil + log.Printf("creating image %s...\n", *image.Metadata.UUID) + err = checkTask(conn, image.Status.ExecutionContext.TaskUUID.(string)) + if err != nil { + return nil, fmt.Errorf("error while Create Image, %s", err.Error()) + } else { + return &nutanixImage{image: *image}, nil + } + } func getEmptyClientSideFilter() []*client.AdditionalFilter { diff --git a/builder/nutanix/step_build_vm.go b/builder/nutanix/step_build_vm.go index 964662c..085b047 100644 --- a/builder/nutanix/step_build_vm.go +++ b/builder/nutanix/step_build_vm.go @@ -2,6 +2,7 @@ package nutanix import ( "context" + "fmt" "log" "github.com/hashicorp/packer-plugin-sdk/multistep" @@ -42,25 +43,27 @@ func (s *stepBuildVM) Run(ctx context.Context, state multistep.StateBag) multist log.Println("no CD disk, not attaching.") } - ui.Say("Creating Packer Builder VM on Nutanix Cluster...") + ui.Say("Creating Packer Builder virtual machine...") //CreateRequest() vmRequest, err := d.CreateRequest(config.VmConfig) if err != nil { - ui.Error("Error creating Request: " + err.Error()) + ui.Error("Error creating virtual machine request: " + err.Error()) state.Put("error", err) return multistep.ActionHalt } vmInstance, err := d.Create(vmRequest) if err != nil { - ui.Error("Unable to create Nutanix VM request: " + err.Error()) + ui.Error("Unable to create virtual machine: " + err.Error()) state.Put("error", err) return multistep.ActionHalt } + + ui.Message(fmt.Sprintf("virtual machine %s created", config.VMName)) log.Printf("Nutanix VM UUID: %s", *vmInstance.nutanix.Metadata.UUID) state.Put("vmUUID", *vmInstance.nutanix.Metadata.UUID) state.Put("ip", vmInstance.Addresses()[0]) - ui.Say("IP for Nutanix device: " + vmInstance.Addresses()[0]) + ui.Message("Found IP for virtual machine: " + vmInstance.Addresses()[0]) return multistep.ActionContinue } diff --git a/builder/nutanix/step_copy_image.go b/builder/nutanix/step_copy_image.go index 866c663..cfbad8c 100644 --- a/builder/nutanix/step_copy_image.go +++ b/builder/nutanix/step_copy_image.go @@ -16,20 +16,19 @@ type stepCopyImage struct { func (s *stepCopyImage) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { ui := state.Get("ui").(packer.Ui) vmUUID := state.Get("vmUUID").(string) - ui.Say("Retrieving Status for uuid: " + vmUUID) d := state.Get("driver").(Driver) vm, _ := d.GetVM(vmUUID) - ui.Say("Creating image for uuid: " + vmUUID) + ui.Say(fmt.Sprintf("Creating image from virtual machine %s...", s.Config.VMName)) - ui.Message("Initiatiating save VM DISK task.") // Choose disk to replicate - looking for first "DISK" var diskToCopy string for i := range vm.nutanix.Spec.Resources.DiskList { if *vm.nutanix.Spec.Resources.DiskList[i].DeviceProperties.DeviceType == "DISK" { diskToCopy = *vm.nutanix.Spec.Resources.DiskList[i].UUID - ui.Message("Found DISK to copy: " + diskToCopy) + diskID := fmt.Sprintf("%s:%d", *vm.nutanix.Spec.Resources.DiskList[i].DeviceProperties.DiskAddress.AdapterType, *vm.nutanix.Spec.Resources.DiskList[i].DeviceProperties.DiskAddress.DeviceIndex) + ui.Message("Found disk to copy: " + diskID) break } } @@ -41,9 +40,9 @@ func (s *stepCopyImage) Run(ctx context.Context, state multistep.StateBag) multi return multistep.ActionHalt } - imageResponse, err := d.SaveVMDisk(diskToCopy) + imageResponse, err := d.SaveVMDisk(diskToCopy, s.Config.ImageCategoryKey, s.Config.ImageCategoryValue) if err != nil { - ui.Error("Unexpected Nutanix Task status: " + err.Error()) + ui.Error("Image creation failed: " + err.Error()) state.Put("error", err) return multistep.ActionHalt } diff --git a/builder/nutanix/step_shutdown_vm.go b/builder/nutanix/step_shutdown_vm.go index d1dda0d..7d4f267 100644 --- a/builder/nutanix/step_shutdown_vm.go +++ b/builder/nutanix/step_shutdown_vm.go @@ -47,7 +47,7 @@ func (s *StepShutdown) Run(ctx context.Context, state multistep.StateBag) multis } else if s.Command != "" { ui.Say("Gracefully halting virtual machine...") - log.Printf("Executing shutdown command: %s", s.Command) + log.Printf("executing shutdown command: %s", s.Command) cmd := &packersdk.RemoteCmd{Command: s.Command} if err := cmd.RunWithUi(ctx, comm, ui); err != nil { err := fmt.Errorf("failed to send shutdown command: %s", err) @@ -67,7 +67,7 @@ func (s *StepShutdown) Run(ctx context.Context, state multistep.StateBag) multis } // Wait for the machine to actually shut down - log.Printf("Waiting max %s for shutdown to complete", s.Timeout) + log.Printf("waiting max %s for shutdown to complete", s.Timeout) shutdownTimer := time.After(s.Timeout) for { running, _ := driver.GetVM(vmUUID)