Skip to content

Add support for OVA export #247

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .web-docs/components/builder/nutanix/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ These parameters allow to configure everything around image creation, from the t
- `force_deregister` (bool) - Allow output image override if already exists.
- `image_delete` (bool) - Delete image once build process is completed (default is false).
- `image_export` (bool) - Export raw image in the current folder (default is false).
- `image_export_type` (bool) - Export image type (default is RAW, accepted values are RAW, VMDK, QCOW2).
- `shutdown_command` (string) - Command line to shutdown your temporary VM.
- `shutdown_timeout` (string) - Timeout for VM shutdown (format : 2m).
- `vm_force_delete` (bool) - Delete vm even if build is not succesful (default is false).
Expand Down
6 changes: 5 additions & 1 deletion builder/nutanix/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ func (b *Builder) Run(ctx context.Context, ui packersdk.Ui, hook packersdk.Hook)
Host: commHost(),
},
new(commonsteps.StepProvision),
&StepExportOVA{
VMName: b.config.VMName,
DiskFileFormat: b.config.ImageExportType,
},
&StepShutdown{
Command: b.config.ShutdownCommand,
Timeout: b.config.ShutdownTimeout,
Expand All @@ -77,7 +81,7 @@ func (b *Builder) Run(ctx context.Context, ui packersdk.Ui, hook packersdk.Hook)
},
}

if b.config.ImageExport {
if b.config.ImageExport && b.config.ImageExportType == "RAW" {
steps = append(steps, &stepExportImage{
VMName: b.config.VMName,
ImageName: b.config.VmConfig.ImageName,
Expand Down
11 changes: 11 additions & 0 deletions builder/nutanix/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ type Config struct {
ImageCategories []Category `mapstructure:"image_categories" required:"false"`
ImageDelete bool `mapstructure:"image_delete" json:"image_delete" required:"false"`
ImageExport bool `mapstructure:"image_export" json:"image_export" required:"false"`
ImageExportType string `mapstructure:"image_export_type" json:"image_export_type" required:"false"`
WaitTimeout time.Duration `mapstructure:"ip_wait_timeout" json:"ip_wait_timeout" required:"false"`
VmForceDelete bool `mapstructure:"vm_force_delete" json:"vm_force_delete" required:"false"`

Expand Down Expand Up @@ -156,6 +157,16 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
c.BootPriority = string(NutanixIdentifierBootPriorityCDROM)
}

if c.ImageExport && c.ImageExportType == "" {
log.Println("Image Export enabled, but no Image Export Type configured. Defaulting to 'RAW'")
c.ImageExportType = "RAW"
}

if c.ImageExportType != "" && c.ImageExportType != "VMDK" && c.ImageExportType != "RAW" && c.ImageExportType != "QCOW2" {
log.Println("Only supported export types are VMDK, RAW and QCOW2")
errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("supported image_export_type is VMDK, RAW or QCOW2"))
}

// Validate Cluster Endpoint
if c.ClusterConfig.Endpoint == "" {
log.Println("Nutanix Endpoint missing from configuration")
Expand Down
2 changes: 2 additions & 0 deletions builder/nutanix/config.hcl2spec.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

178 changes: 170 additions & 8 deletions builder/nutanix/driver.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package nutanix

import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"log"
Expand Down Expand Up @@ -35,6 +37,7 @@ type Driver interface {
CreateImageFile(context.Context, string, VmConfig) (*nutanixImage, error)
DeleteImage(context.Context, string) error
GetImage(context.Context, string) (*nutanixImage, error)
ExportOVA(context.Context, string, string) (io.ReadCloser, error)
ExportImage(context.Context, string) (io.ReadCloser, error)
SaveVMDisk(context.Context, string, int, []Category) (*nutanixImage, error)
WaitForShutdown(string, <-chan struct{}) bool
Expand Down Expand Up @@ -234,11 +237,11 @@ func findImageByName(ctx context.Context, conn *v3.Client, name string) (*v3.Ima
return findImageByUUID(ctx, conn, *found[0].Metadata.UUID)
}

func checkTask(ctx context.Context, conn *v3.Client, taskUUID string) error {
func checkTask(ctx context.Context, conn *v3.Client, taskUUID string, timeout int) error {
log.Printf("checking task %s...", taskUUID)
var task *v3.TasksResponse
var err error
for i := 0; i < 120; i++ {
for i := 0; i < (timeout / 5); i++ {
task, err = conn.V3.GetTask(ctx, taskUUID)
if err == nil {
if *task.Status == "SUCCEEDED" {
Expand Down Expand Up @@ -628,7 +631,7 @@ func (d *NutanixDriver) Create(ctx context.Context, req *v3.VMIntentInput) (*nut

uuid := *resp.Metadata.UUID

err = checkTask(ctx, conn, resp.Status.ExecutionContext.TaskUUID.(string))
err = checkTask(ctx, conn, resp.Status.ExecutionContext.TaskUUID.(string), 600)

if err != nil {
log.Printf("error creating vm: %s", err.Error())
Expand Down Expand Up @@ -762,7 +765,7 @@ func (d *NutanixDriver) CreateImageURL(ctx context.Context, disk VmDisk, vm VmCo
return nil, fmt.Errorf("error while creating image: %s", err.Error())
}

err = checkTask(ctx, conn, image.Status.ExecutionContext.TaskUUID.(string))
err = checkTask(ctx, conn, image.Status.ExecutionContext.TaskUUID.(string), 600)
if err != nil {
return nil, fmt.Errorf("error while creating image: %s", err.Error())
}
Expand Down Expand Up @@ -824,7 +827,7 @@ func (d *NutanixDriver) CreateImageFile(ctx context.Context, filePath string, vm
return nil, fmt.Errorf("error while creating image: %s", err.Error())
}

err = checkTask(ctx, conn, image.Status.ExecutionContext.TaskUUID.(string))
err = checkTask(ctx, conn, image.Status.ExecutionContext.TaskUUID.(string), 600)
if err != nil {
return nil, fmt.Errorf("error while creating image: %s", err.Error())
}
Expand Down Expand Up @@ -909,6 +912,165 @@ func (d *NutanixDriver) GetVM(ctx context.Context, vmUUID string) (*nutanixInsta
return &nutanixInstance{nutanix: *vm}, nil
}

func (d *NutanixDriver) getRequest(ctx context.Context, url string) (*http.Response, error) {
customTransport := http.DefaultTransport.(*http.Transport).Clone()
customTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: d.ClusterConfig.Insecure}
httpClient := &http.Client{Transport: customTransport}

req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
req = req.WithContext(ctx)
req.SetBasicAuth(d.ClusterConfig.Username, d.ClusterConfig.Password)
resp, err := httpClient.Do(req)
if err != nil {
return nil, err
}

if resp.StatusCode > 200 && resp.StatusCode < 300 {
return nil, fmt.Errorf(resp.Status)
}
return resp, nil
}

func (d *NutanixDriver) postRequest(ctx context.Context, url string, payload map[string]string) (*http.Response, error) {
customTransport := http.DefaultTransport.(*http.Transport).Clone()
customTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: d.ClusterConfig.Insecure}
httpClient := &http.Client{Transport: customTransport}

jsonData, err := json.Marshal(payload)
if err != nil {
return nil, err
}

req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")

req = req.WithContext(ctx)
req.SetBasicAuth(d.ClusterConfig.Username, d.ClusterConfig.Password)
resp, err := httpClient.Do(req)
if err != nil {
return nil, err
}

if resp.StatusCode < 200 || resp.StatusCode > 300 {
err_return := fmt.Errorf(resp.Status)
bodyBytes, err := io.ReadAll(resp.Body)
if err == nil {
return nil, fmt.Errorf("request returned non-200 status: %s, Response Body: %s", resp.Status, string(bodyBytes))
}
return nil, err_return
}
return resp, nil
}

func GetOVAByName(ctx context.Context, entityType string, vmUUID string, conn *v3.Client) string {
request := v3.GroupsGetEntitiesRequest{
EntityType: &entityType,
FilterCriteria: fmt.Sprintf(`name==%s`, vmUUID),
}

var response *v3.GroupsGetEntitiesResponse
response, err := conn.V3.GroupsGetEntities(ctx, &request)

if err != nil {
if response != nil {
log.Printf("Partial response: %+v", response)
}
} else {
groupResults := response.GroupResults
if len(groupResults) > 0 {
entityList := groupResults[0].EntityResults
if len(entityList) > 0 {
return entityList[0].EntityID
}
}
}
return ""
}

func (d *NutanixDriver) createExportOVATask(ctx context.Context, vmUUID string, ovaName string, diskFileFormat string) (string, error) {
url := fmt.Sprintf("https://%s:%d/api/nutanix/v3/vms/%s/export", d.ClusterConfig.Endpoint, d.ClusterConfig.Port, vmUUID)
log.Printf("export ova using api: %s", url)

payload := map[string]string{
"name": ovaName,
"disk_file_format": diskFileFormat,
}

resp, err := d.postRequest(ctx, url, payload)
if err != nil {
return "", err
}

var result struct {
TaskUUID string `json:"task_uuid"`
}
err = json.NewDecoder(resp.Body).Decode(&result)
if err != nil {
return "", err
}
log.Printf("export task created with Task UUID: %s", result.TaskUUID)
return result.TaskUUID, nil
}

func (d *NutanixDriver) ExportOVA(ctx context.Context, vmUUID string, diskFileFormat string) (io.ReadCloser, error) {
log.Printf("starting OVA export for VM UUID: %s with disk format: %s", vmUUID, diskFileFormat)
configCreds := client.Credentials{
URL: fmt.Sprintf("%s:%d", d.ClusterConfig.Endpoint, d.ClusterConfig.Port),
Endpoint: d.ClusterConfig.Endpoint,
Username: d.ClusterConfig.Username,
Password: d.ClusterConfig.Password,
Port: string(d.ClusterConfig.Port),
Insecure: d.ClusterConfig.Insecure,
}

conn, err := v3.NewV3Client(configCreds)
if err != nil {
return nil, fmt.Errorf("error while NewV3Client, %s", err.Error())
}

ovaName := fmt.Sprintf("packer-export-%s", vmUUID)
taskUUID, err := d.createExportOVATask(ctx, vmUUID, ovaName, diskFileFormat)
if err != nil {
return nil, err
}

err = checkTask(ctx, conn, taskUUID, 3600)
if err != nil {
return nil, err
}

log.Printf("OVA export task %s completed successfully.", taskUUID)

var ovaUUID string

// Recheck for a little while to make sure the OVA with name ovaName appears
for i := 0; i < (60 / 5); i++ {
ovaUUID = GetOVAByName(ctx, "ova", ovaName, conn)
if ovaUUID == "" {
<-time.After(5 * time.Second)
}
}

if ovaUUID == "" {
return nil, fmt.Errorf("timeout waiting for OVA entity to appear")
}

ova_download_url := fmt.Sprintf("https://%s:%d/api/nutanix/v3/ovas/%s/file", d.ClusterConfig.Endpoint, d.ClusterConfig.Port, ovaUUID)
log.Printf("The ova download url is: %s", ova_download_url)
ova_resp, err := d.getRequest(ctx, ova_download_url)
if err != nil {
return nil, err
}
return ova_resp.Body, nil
}

func (d *NutanixDriver) ExportImage(ctx context.Context, imageUUID string) (io.ReadCloser, error) {
customTransport := http.DefaultTransport.(*http.Transport).Clone()
customTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: d.ClusterConfig.Insecure}
Expand Down Expand Up @@ -991,7 +1153,7 @@ func (d *NutanixDriver) PowerOff(ctx context.Context, vmUUID string) error {

// Wait for the VM to be stopped
log.Printf("stopping VM: %s", d.Config.VMName)
err = checkTask(ctx, conn, taskUUID)
err = checkTask(ctx, conn, taskUUID, 600)
if err != nil {
return fmt.Errorf("error while stopping VM: %s", err.Error())
}
Expand Down Expand Up @@ -1049,7 +1211,7 @@ func (d *NutanixDriver) SaveVMDisk(ctx context.Context, diskUUID string, index i
}

log.Printf("deleting image %s...\n", *found[0].Metadata.UUID)
err = checkTask(ctx, conn, resp.Status.ExecutionContext.TaskUUID.(string))
err = checkTask(ctx, conn, resp.Status.ExecutionContext.TaskUUID.(string), 600)

if err != nil {
return nil, fmt.Errorf("error while Deleting Image, %s", err.Error())
Expand Down Expand Up @@ -1089,7 +1251,7 @@ func (d *NutanixDriver) SaveVMDisk(ctx context.Context, diskUUID string, index i
return nil, fmt.Errorf("error while Creating Image, %s", err.Error())
}
log.Printf("creating image %s...\n", *image.Metadata.UUID)
err = checkTask(ctx, conn, image.Status.ExecutionContext.TaskUUID.(string))
err = checkTask(ctx, conn, image.Status.ExecutionContext.TaskUUID.(string), 600)
if err != nil {
return nil, fmt.Errorf("error while Creating Image, %s", err.Error())
} else {
Expand Down
Loading
Loading