2020-03-19 13:51:43 -04:00
//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"
2020-11-12 17:44:02 -05:00
"github.com/hashicorp/packer/packer-plugin-sdk/common"
2020-11-17 19:31:03 -05:00
"github.com/hashicorp/packer/packer-plugin-sdk/multistep"
2020-11-19 14:54:31 -05:00
packersdk "github.com/hashicorp/packer/packer-plugin-sdk/packer"
2020-11-11 13:21:37 -05:00
"github.com/hashicorp/packer/packer-plugin-sdk/template/interpolate"
2020-03-19 13:51:43 -04:00
"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:
//
2020-08-05 13:23:52 -04:00
// In JSON:
2020-03-19 13:51:43 -04:00
// ```json
// ...
// "vm_name": "example-ubuntu",
// ...
// "export": {
// "force": true,
// "output_directory": "./output_vsphere"
// },
// ```
2020-08-05 13:23:52 -04:00
// In HCL2:
// ```hcl
// # ...
// vm_name = "example-ubuntu"
// # ...
// export {
// force = true
// output_directory = "./output_vsphere"
// }
// ```
2020-03-19 13:51:43 -04:00
// The above configuration would create the following files:
//
2020-03-18 18:46:47 -04:00
// ```text
2020-03-19 13:51:43 -04:00
// ./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.
2020-04-30 15:22:57 -04:00
Manifest string ` mapstructure:"manifest" `
// Directory on the computer running Packer to export files to
2020-03-19 13:51:43 -04:00
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:
2020-03-19 14:46:30 -04:00
//
2020-08-05 13:23:52 -04:00
// In JSON:
2020-03-19 13:51:43 -04:00
// ```json
// ...
// "export": {
// "options": ["mac"]
// },
// ```
2020-08-05 13:23:52 -04:00
// In HCL2:
// ```hcl
// ...
// export {
// options = ["mac"]
// }
// ```
2020-03-19 13:51:43 -04:00
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 {
2020-11-19 15:07:02 -05:00
var errs * packersdk . MultiError
2020-03-19 13:51:43 -04:00
2020-11-19 15:07:02 -05:00
errs = packersdk . MultiErrorAppend ( errs , c . OutputDir . Prepare ( ctx , pc ) ... )
2020-03-19 13:51:43 -04:00
// manifest should default to sha256
if c . Manifest == "" {
c . Manifest = "sha256"
}
if _ , ok := sha [ c . Manifest ] ; ! ok {
2020-11-19 15:07:02 -05:00
errs = packersdk . MultiErrorAppend ( errs , fmt . Errorf ( "unknown hash: %s. available options include available options being 'none', 'sha1', 'sha256', 'sha512'" , c . Manifest ) )
2020-03-19 13:51:43 -04:00
}
if c . Name == "" {
c . Name = lc . VMName
}
target := getTarget ( c . OutputDir . OutputDir , c . Name )
if ! c . Force {
if _ , err := os . Stat ( target ) ; err == nil {
2020-11-19 15:07:02 -05:00
errs = packersdk . MultiErrorAppend ( errs , fmt . Errorf ( "file already exists: %s" , target ) )
2020-03-19 13:51:43 -04:00
}
}
2020-08-02 02:02:31 -04:00
if err := os . MkdirAll ( c . OutputDir . OutputDir , c . OutputDir . DirPerm ) ; err != nil {
2020-11-19 15:07:02 -05:00
errs = packersdk . MultiErrorAppend ( errs , errors . Wrap ( err , "unable to make directory for export" ) )
2020-03-19 13:51:43 -04:00
}
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 {
2020-11-19 14:54:31 -05:00
ui := state . Get ( "ui" ) . ( packersdk . Ui )
2020-08-31 04:34:41 -04:00
vm := state . Get ( "vm" ) . ( * driver . VirtualMachineDriver )
2020-03-19 13:51:43 -04:00
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
}
2020-07-14 05:36:49 -04:00
file := i . File ( )
ui . Message ( "Downloading: " + file . Path )
size , err := s . Download ( ctx , lease , i )
2020-03-19 13:51:43 -04:00
if err != nil {
state . Put ( "error" , err )
return multistep . ActionHalt
}
2020-07-14 05:36:49 -04:00
// Fix file size descriptor
file . Size = size
ui . Message ( "Exporting file: " + file . Path )
cdp . OvfFiles = append ( cdp . OvfFiles , file )
2020-03-19 13:51:43 -04:00
}
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 ) )
}
2020-07-14 05:36:49 -04:00
func ( s * StepExport ) Download ( ctx context . Context , lease * nfc . Lease , item nfc . FileItem ) ( int64 , error ) {
2020-03-19 13:51:43 -04:00
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 )
}
2020-07-14 05:36:49 -04:00
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
2020-03-19 13:51:43 -04:00
}