320 lines
7.8 KiB
Go
320 lines
7.8 KiB
Go
//go:generate struct-markdown
|
|
//go:generate mapstructure-to-hcl2 -type ExportConfig
|
|
|
|
package common
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/sha1"
|
|
"crypto/sha256"
|
|
"crypto/sha512"
|
|
"fmt"
|
|
"hash"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/packer/builder/vsphere/driver"
|
|
"github.com/hashicorp/packer/common"
|
|
"github.com/hashicorp/packer/helper/multistep"
|
|
"github.com/hashicorp/packer/packer"
|
|
"github.com/hashicorp/packer/template/interpolate"
|
|
"github.com/pkg/errors"
|
|
"github.com/vmware/govmomi/nfc"
|
|
"github.com/vmware/govmomi/vim25/soap"
|
|
"github.com/vmware/govmomi/vim25/types"
|
|
)
|
|
|
|
// You may optionally export an ovf from VSphere to the instance running Packer.
|
|
//
|
|
// Example usage:
|
|
//
|
|
// ```json
|
|
// ...
|
|
// "vm_name": "example-ubuntu",
|
|
// ...
|
|
// "export": {
|
|
// "force": true,
|
|
// "output_directory": "./output_vsphere"
|
|
// },
|
|
// ```
|
|
// The above configuration would create the following files:
|
|
//
|
|
// ```text
|
|
// ./output_vsphere/example-ubuntu-disk-0.vmdk
|
|
// ./output_vsphere/example-ubuntu.mf
|
|
// ./output_vsphere/example-ubuntu.ovf
|
|
// ```
|
|
type ExportConfig struct {
|
|
// name of the ovf. defaults to the name of the VM
|
|
Name string `mapstructure:"name"`
|
|
// overwrite ovf if it exists
|
|
Force bool `mapstructure:"force"`
|
|
// include iso and img image files that are attached to the VM
|
|
Images bool `mapstructure:"images"`
|
|
// generate manifest using sha1, sha256, sha512. Defaults to 'sha256'. Use 'none' for no manifest.
|
|
Manifest string `mapstructure:"manifest"`
|
|
// Directory on the computer running Packer to export files to
|
|
OutputDir OutputConfig `mapstructure:",squash"`
|
|
// Advanced ovf export options. Options can include:
|
|
// * mac - MAC address is exported for all ethernet devices
|
|
// * uuid - UUID is exported for all virtual machines
|
|
// * extraconfig - all extra configuration options are exported for a virtual machine
|
|
// * nodevicesubtypes - resource subtypes for CD/DVD drives, floppy drives, and serial and parallel ports are not exported
|
|
//
|
|
// For example, adding the following export config option would output the mac addresses for all Ethernet devices in the ovf file:
|
|
//
|
|
// ```json
|
|
// ...
|
|
// "export": {
|
|
// "options": ["mac"]
|
|
// },
|
|
// ```
|
|
Options []string `mapstructure:"options"`
|
|
}
|
|
|
|
var sha = map[string]func() hash.Hash{
|
|
"none": nil,
|
|
"sha1": sha1.New,
|
|
"sha256": sha256.New,
|
|
"sha512": sha512.New,
|
|
}
|
|
|
|
func (c *ExportConfig) Prepare(ctx *interpolate.Context, lc *LocationConfig, pc *common.PackerConfig) []error {
|
|
var errs *packer.MultiError
|
|
|
|
errs = packer.MultiErrorAppend(errs, c.OutputDir.Prepare(ctx, pc)...)
|
|
|
|
// manifest should default to sha256
|
|
if c.Manifest == "" {
|
|
c.Manifest = "sha256"
|
|
}
|
|
if _, ok := sha[c.Manifest]; !ok {
|
|
errs = packer.MultiErrorAppend(errs, fmt.Errorf("unknown hash: %s. available options include available options being 'none', 'sha1', 'sha256', 'sha512'", c.Manifest))
|
|
}
|
|
|
|
if c.Name == "" {
|
|
c.Name = lc.VMName
|
|
}
|
|
target := getTarget(c.OutputDir.OutputDir, c.Name)
|
|
if !c.Force {
|
|
if _, err := os.Stat(target); err == nil {
|
|
errs = packer.MultiErrorAppend(errs, fmt.Errorf("file already exists: %s", target))
|
|
}
|
|
}
|
|
|
|
if err := os.MkdirAll(c.OutputDir.OutputDir, 0750); err != nil {
|
|
errs = packer.MultiErrorAppend(errs, errors.Wrap(err, "unable to make directory for export"))
|
|
}
|
|
|
|
if errs != nil && len(errs.Errors) > 0 {
|
|
return errs.Errors
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func getTarget(dir string, name string) string {
|
|
return filepath.Join(dir, name+".ovf")
|
|
}
|
|
|
|
type StepExport struct {
|
|
Name string
|
|
Force bool
|
|
Images bool
|
|
Manifest string
|
|
OutputDir string
|
|
Options []string
|
|
mf bytes.Buffer
|
|
}
|
|
|
|
func (s *StepExport) Cleanup(multistep.StateBag) {
|
|
}
|
|
|
|
func (s *StepExport) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
|
ui := state.Get("ui").(packer.Ui)
|
|
vm := state.Get("vm").(*driver.VirtualMachine)
|
|
|
|
ui.Message("Starting export...")
|
|
lease, err := vm.Export()
|
|
if err != nil {
|
|
state.Put("error", errors.Wrap(err, "error exporting vm"))
|
|
return multistep.ActionHalt
|
|
}
|
|
|
|
info, err := lease.Wait(ctx, nil)
|
|
if err != nil {
|
|
state.Put("error", err)
|
|
return multistep.ActionHalt
|
|
}
|
|
|
|
u := lease.StartUpdater(ctx, info)
|
|
defer u.Done()
|
|
|
|
cdp := types.OvfCreateDescriptorParams{
|
|
Name: s.Name,
|
|
}
|
|
|
|
m := vm.NewOvfManager()
|
|
if len(s.Options) > 0 {
|
|
exportOptions, err := vm.GetOvfExportOptions(m)
|
|
if err != nil {
|
|
state.Put("error", err)
|
|
return multistep.ActionHalt
|
|
}
|
|
var unknown []string
|
|
for _, option := range s.Options {
|
|
found := false
|
|
for _, exportOpt := range exportOptions {
|
|
if exportOpt.Option == option {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
unknown = append(unknown, option)
|
|
}
|
|
cdp.ExportOption = append(cdp.ExportOption, option)
|
|
}
|
|
|
|
// only printing error message because the unknown options are just ignored by vcenter
|
|
if len(unknown) > 0 {
|
|
ui.Error(fmt.Sprintf("unknown export options %s", strings.Join(unknown, ",")))
|
|
}
|
|
}
|
|
|
|
for _, i := range info.Items {
|
|
if !s.include(&i) {
|
|
continue
|
|
}
|
|
|
|
if !strings.HasPrefix(i.Path, s.Name) {
|
|
i.Path = s.Name + "-" + i.Path
|
|
}
|
|
|
|
file := i.File()
|
|
|
|
ui.Message("Downloading: " + file.Path)
|
|
size, err := s.Download(ctx, lease, i)
|
|
if err != nil {
|
|
state.Put("error", err)
|
|
return multistep.ActionHalt
|
|
}
|
|
|
|
// Fix file size descriptor
|
|
file.Size = size
|
|
|
|
ui.Message("Exporting file: " + file.Path)
|
|
cdp.OvfFiles = append(cdp.OvfFiles, file)
|
|
}
|
|
|
|
if err = lease.Complete(ctx); err != nil {
|
|
state.Put("error", errors.Wrap(err, "unable to complete lease"))
|
|
return multistep.ActionHalt
|
|
}
|
|
|
|
desc, err := vm.CreateDescriptor(m, cdp)
|
|
if err != nil {
|
|
state.Put("error", errors.Wrap(err, "unable to create descriptor"))
|
|
return multistep.ActionHalt
|
|
}
|
|
|
|
target := getTarget(s.OutputDir, s.Name)
|
|
file, err := os.Create(target)
|
|
if err != nil {
|
|
state.Put("error", errors.Wrap(err, "unable to create file: "+target))
|
|
return multistep.ActionHalt
|
|
}
|
|
|
|
var w io.Writer = file
|
|
h, ok := s.newHash()
|
|
if ok {
|
|
w = io.MultiWriter(file, h)
|
|
}
|
|
|
|
ui.Message("Writing ovf...")
|
|
_, err = io.WriteString(w, desc.OvfDescriptor)
|
|
if err != nil {
|
|
state.Put("error", errors.Wrap(err, "unable to write descriptor"))
|
|
return multistep.ActionHalt
|
|
}
|
|
|
|
if err = file.Close(); err != nil {
|
|
state.Put("error", errors.Wrap(err, "unable to close descriptor"))
|
|
return multistep.ActionHalt
|
|
}
|
|
|
|
if s.Manifest == "none" {
|
|
// manifest does not need to be created, return
|
|
return multistep.ActionContinue
|
|
}
|
|
|
|
ui.Message("Creating manifest...")
|
|
s.addHash(filepath.Base(target), h)
|
|
|
|
file, err = os.Create(filepath.Join(s.OutputDir, s.Name+".mf"))
|
|
if err != nil {
|
|
state.Put("error", errors.Wrap(err, "unable to create manifest"))
|
|
return multistep.ActionHalt
|
|
}
|
|
|
|
_, err = io.Copy(file, &s.mf)
|
|
if err != nil {
|
|
state.Put("error", errors.Wrap(err, "unable to write manifest"))
|
|
return multistep.ActionHalt
|
|
}
|
|
|
|
err = file.Close()
|
|
if err != nil {
|
|
state.Put("error", errors.Wrap(err, "unable to close file"))
|
|
return multistep.ActionHalt
|
|
}
|
|
|
|
ui.Message("Finished exporting...")
|
|
return multistep.ActionContinue
|
|
}
|
|
|
|
func (s *StepExport) include(item *nfc.FileItem) bool {
|
|
if s.Images {
|
|
return true
|
|
}
|
|
|
|
return filepath.Ext(item.Path) == ".vmdk"
|
|
}
|
|
|
|
func (s *StepExport) newHash() (hash.Hash, bool) {
|
|
// check if function is nil to handle the 'none' case
|
|
if h, ok := sha[s.Manifest]; ok && h != nil {
|
|
return h(), true
|
|
}
|
|
|
|
return nil, false
|
|
}
|
|
|
|
func (s *StepExport) addHash(p string, h hash.Hash) {
|
|
_, _ = fmt.Fprintf(&s.mf, "%s(%s)= %x\n", strings.ToUpper(s.Manifest), p, h.Sum(nil))
|
|
}
|
|
|
|
func (s *StepExport) Download(ctx context.Context, lease *nfc.Lease, item nfc.FileItem) (int64, error) {
|
|
path := filepath.Join(s.OutputDir, item.Path)
|
|
opts := soap.Download{}
|
|
|
|
if h, ok := s.newHash(); ok {
|
|
opts.Writer = h
|
|
defer s.addHash(item.Path, h)
|
|
}
|
|
|
|
err := lease.DownloadFile(ctx, path, item, opts)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
f, err := os.Stat(path)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return f.Size(), err
|
|
}
|