From d2dd158fe22ece055f62adf62dd88c00f1c6071d Mon Sep 17 00:00:00 2001 From: Megan Marsh Date: Wed, 19 Aug 2020 14:09:08 -0700 Subject: [PATCH 1/4] Implement cd_files option. The Addfile code is modified from the floppy_files tooling. Tested on OSX, `Windows, and Linux. Documented command line dependencies. Added acceptance tests (behind testacc flag, since there are dependencies). This option is only implemented for the vmware and hyperv iso builders. It will be implemented for others in later PRs in order to make review easier. --- builder/hyperv/common/config.go | 5 +- .../common/step_mount_secondary_dvd_images.go | 12 +- builder/hyperv/iso/builder.go | 5 +- builder/hyperv/iso/builder.hcl2spec.go | 4 + builder/hyperv/vmcx/builder.hcl2spec.go | 4 + builder/vmware/iso/builder.go | 10 + builder/vmware/iso/config.go | 2 + builder/vmware/iso/config.hcl2spec.go | 4 + builder/vmware/iso/step_create_vmx.go | 17 ++ common/extra_iso_config.go | 98 +++++++++ common/extra_iso_config_test.go | 73 +++++++ common/step_create_cdrom.go | 201 ++++++++++++++++++ common/step_create_cdrom_test.go | 119 +++++++++++ go.sum | 7 +- website/pages/docs/builders/hyperv/iso.mdx | 8 +- website/pages/docs/builders/vmware/iso.mdx | 4 + .../partials/common/CDConfig-not-required.mdx | 46 ++++ website/pages/partials/common/CDConfig.mdx | 11 + 18 files changed, 622 insertions(+), 8 deletions(-) create mode 100644 common/extra_iso_config.go create mode 100644 common/extra_iso_config_test.go create mode 100644 common/step_create_cdrom.go create mode 100644 common/step_create_cdrom_test.go create mode 100644 website/pages/partials/common/CDConfig-not-required.mdx create mode 100644 website/pages/partials/common/CDConfig.mdx diff --git a/builder/hyperv/common/config.go b/builder/hyperv/common/config.go index 53a3e9e8a..5ccd5d453 100644 --- a/builder/hyperv/common/config.go +++ b/builder/hyperv/common/config.go @@ -37,6 +37,7 @@ const ( type CommonConfig struct { common.FloppyConfig `mapstructure:",squash"` + common.CDConfig `mapstructure:",squash"` // The block size of the VHD to be created. // Recommended disk block size for Linux hyper-v guests is 1 MiB. This // defaults to "32" MiB. @@ -210,8 +211,8 @@ func (c *CommonConfig) Prepare(ctx *interpolate.Context, pc *common.PackerConfig } // Errors - floppyerrs := c.FloppyConfig.Prepare(ctx) - errs = append(errs, floppyerrs...) + errs = append(errs, c.FloppyConfig.Prepare(ctx)...) + errs = append(errs, c.CDConfig.Prepare(ctx)...) if c.GuestAdditionsMode == "" { if c.GuestAdditionsPath != "" { c.GuestAdditionsMode = "attach" diff --git a/builder/hyperv/common/step_mount_secondary_dvd_images.go b/builder/hyperv/common/step_mount_secondary_dvd_images.go index 4303f51f4..a6cbd7e6b 100644 --- a/builder/hyperv/common/step_mount_secondary_dvd_images.go +++ b/builder/hyperv/common/step_mount_secondary_dvd_images.go @@ -34,7 +34,17 @@ func (s *StepMountSecondaryDvdImages) Run(ctx context.Context, state multistep.S // For IDE, there are only 2 controllers (0,1) with 2 locations each (0,1) var dvdProperties []DvdControllerProperties - for _, isoPath := range s.IsoPaths { + isoPaths := s.IsoPaths + + // Add our custom CD, if it exists + cd_path, ok := state.Get("cd_path").(string) + if ok { + if cd_path != "" { + isoPaths = append(isoPaths, cd_path) + } + } + + for _, isoPath := range isoPaths { var properties DvdControllerProperties controllerNumber, controllerLocation, err := driver.CreateDvdDrive(vmName, isoPath, s.Generation) diff --git a/builder/hyperv/iso/builder.go b/builder/hyperv/iso/builder.go index 7af2ab1ba..25284b0b2 100644 --- a/builder/hyperv/iso/builder.go +++ b/builder/hyperv/iso/builder.go @@ -254,7 +254,10 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack GuestAdditionsPath: b.config.GuestAdditionsPath, Generation: b.config.Generation, }, - + &common.StepCreateCD{ + Files: b.config.CDConfig.CDFiles, + Label: b.config.CDConfig.CDLabel, + }, &hypervcommon.StepMountSecondaryDvdImages{ IsoPaths: b.config.SecondaryDvdImages, Generation: b.config.Generation, diff --git a/builder/hyperv/iso/builder.hcl2spec.go b/builder/hyperv/iso/builder.hcl2spec.go index 42a7b2c40..23591cf1c 100644 --- a/builder/hyperv/iso/builder.hcl2spec.go +++ b/builder/hyperv/iso/builder.hcl2spec.go @@ -79,6 +79,8 @@ type FlatConfig struct { FloppyFiles []string `mapstructure:"floppy_files" cty:"floppy_files" hcl:"floppy_files"` FloppyDirectories []string `mapstructure:"floppy_dirs" cty:"floppy_dirs" hcl:"floppy_dirs"` FloppyLabel *string `mapstructure:"floppy_label" cty:"floppy_label" hcl:"floppy_label"` + CDFiles []string `mapstructure:"cd_files" cty:"cd_files" hcl:"cd_files"` + CDLabel *string `mapstructure:"cd_label" cty:"cd_label" hcl:"cd_label"` DiskBlockSize *uint `mapstructure:"disk_block_size" required:"false" cty:"disk_block_size" hcl:"disk_block_size"` RamSize *uint `mapstructure:"memory" required:"false" cty:"memory" hcl:"memory"` SecondaryDvdImages []string `mapstructure:"secondary_iso_images" required:"false" cty:"secondary_iso_images" hcl:"secondary_iso_images"` @@ -195,6 +197,8 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec { "floppy_files": &hcldec.AttrSpec{Name: "floppy_files", Type: cty.List(cty.String), Required: false}, "floppy_dirs": &hcldec.AttrSpec{Name: "floppy_dirs", Type: cty.List(cty.String), Required: false}, "floppy_label": &hcldec.AttrSpec{Name: "floppy_label", Type: cty.String, Required: false}, + "cd_files": &hcldec.AttrSpec{Name: "cd_files", Type: cty.List(cty.String), Required: false}, + "cd_label": &hcldec.AttrSpec{Name: "cd_label", Type: cty.String, Required: false}, "disk_block_size": &hcldec.AttrSpec{Name: "disk_block_size", Type: cty.Number, Required: false}, "memory": &hcldec.AttrSpec{Name: "memory", Type: cty.Number, Required: false}, "secondary_iso_images": &hcldec.AttrSpec{Name: "secondary_iso_images", Type: cty.List(cty.String), Required: false}, diff --git a/builder/hyperv/vmcx/builder.hcl2spec.go b/builder/hyperv/vmcx/builder.hcl2spec.go index ff9806d9e..89cc7e27f 100644 --- a/builder/hyperv/vmcx/builder.hcl2spec.go +++ b/builder/hyperv/vmcx/builder.hcl2spec.go @@ -79,6 +79,8 @@ type FlatConfig struct { FloppyFiles []string `mapstructure:"floppy_files" cty:"floppy_files" hcl:"floppy_files"` FloppyDirectories []string `mapstructure:"floppy_dirs" cty:"floppy_dirs" hcl:"floppy_dirs"` FloppyLabel *string `mapstructure:"floppy_label" cty:"floppy_label" hcl:"floppy_label"` + CDFiles []string `mapstructure:"cd_files" cty:"cd_files" hcl:"cd_files"` + CDLabel *string `mapstructure:"cd_label" cty:"cd_label" hcl:"cd_label"` DiskBlockSize *uint `mapstructure:"disk_block_size" required:"false" cty:"disk_block_size" hcl:"disk_block_size"` RamSize *uint `mapstructure:"memory" required:"false" cty:"memory" hcl:"memory"` SecondaryDvdImages []string `mapstructure:"secondary_iso_images" required:"false" cty:"secondary_iso_images" hcl:"secondary_iso_images"` @@ -197,6 +199,8 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec { "floppy_files": &hcldec.AttrSpec{Name: "floppy_files", Type: cty.List(cty.String), Required: false}, "floppy_dirs": &hcldec.AttrSpec{Name: "floppy_dirs", Type: cty.List(cty.String), Required: false}, "floppy_label": &hcldec.AttrSpec{Name: "floppy_label", Type: cty.String, Required: false}, + "cd_files": &hcldec.AttrSpec{Name: "cd_files", Type: cty.List(cty.String), Required: false}, + "cd_label": &hcldec.AttrSpec{Name: "cd_label", Type: cty.String, Required: false}, "disk_block_size": &hcldec.AttrSpec{Name: "disk_block_size", Type: cty.Number, Required: false}, "memory": &hcldec.AttrSpec{Name: "memory", Type: cty.Number, Required: false}, "secondary_iso_images": &hcldec.AttrSpec{Name: "secondary_iso_images", Type: cty.List(cty.String), Required: false}, diff --git a/builder/vmware/iso/builder.go b/builder/vmware/iso/builder.go index 624bd8c86..a57e2d5c8 100644 --- a/builder/vmware/iso/builder.go +++ b/builder/vmware/iso/builder.go @@ -76,12 +76,22 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack Directories: b.config.FloppyConfig.FloppyDirectories, Label: b.config.FloppyConfig.FloppyLabel, }, + &common.StepCreateCD{ + Files: b.config.CDConfig.CDFiles, + Label: b.config.CDConfig.CDLabel, + }, &vmwcommon.StepRemoteUpload{ Key: "floppy_path", Message: "Uploading Floppy to remote machine...", DoCleanup: true, Checksum: "none", }, + &vmwcommon.StepRemoteUpload{ + Key: "cd_path", + Message: "Uploading CD to remote machine...", + DoCleanup: true, + Checksum: "none", + }, &vmwcommon.StepRemoteUpload{ Key: "iso_path", Message: "Uploading ISO to remote machine...", diff --git a/builder/vmware/iso/config.go b/builder/vmware/iso/config.go index dcb5d0faf..25c987d99 100644 --- a/builder/vmware/iso/config.go +++ b/builder/vmware/iso/config.go @@ -23,6 +23,7 @@ type Config struct { common.HTTPConfig `mapstructure:",squash"` common.ISOConfig `mapstructure:",squash"` common.FloppyConfig `mapstructure:",squash"` + common.CDConfig `mapstructure:",squash"` bootcommand.VNCConfig `mapstructure:",squash"` vmwcommon.DriverConfig `mapstructure:",squash"` vmwcommon.HWConfig `mapstructure:",squash"` @@ -110,6 +111,7 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) { errs = packer.MultiErrorAppend(errs, c.ToolsConfig.Prepare(&c.ctx)...) errs = packer.MultiErrorAppend(errs, c.VMXConfig.Prepare(&c.ctx)...) errs = packer.MultiErrorAppend(errs, c.FloppyConfig.Prepare(&c.ctx)...) + errs = packer.MultiErrorAppend(errs, c.CDConfig.Prepare(&c.ctx)...) errs = packer.MultiErrorAppend(errs, c.VNCConfig.Prepare(&c.ctx)...) errs = packer.MultiErrorAppend(errs, c.ExportConfig.Prepare(&c.ctx)...) errs = packer.MultiErrorAppend(errs, c.DiskConfig.Prepare(&c.ctx)...) diff --git a/builder/vmware/iso/config.hcl2spec.go b/builder/vmware/iso/config.hcl2spec.go index 5ee9f0283..345eb4c38 100644 --- a/builder/vmware/iso/config.hcl2spec.go +++ b/builder/vmware/iso/config.hcl2spec.go @@ -28,6 +28,8 @@ type FlatConfig struct { FloppyFiles []string `mapstructure:"floppy_files" cty:"floppy_files" hcl:"floppy_files"` FloppyDirectories []string `mapstructure:"floppy_dirs" cty:"floppy_dirs" hcl:"floppy_dirs"` FloppyLabel *string `mapstructure:"floppy_label" cty:"floppy_label" hcl:"floppy_label"` + CDFiles []string `mapstructure:"cd_files" cty:"cd_files" hcl:"cd_files"` + CDLabel *string `mapstructure:"cd_label" cty:"cd_label" hcl:"cd_label"` BootGroupInterval *string `mapstructure:"boot_keygroup_interval" cty:"boot_keygroup_interval" hcl:"boot_keygroup_interval"` BootWait *string `mapstructure:"boot_wait" cty:"boot_wait" hcl:"boot_wait"` BootCommand []string `mapstructure:"boot_command" cty:"boot_command" hcl:"boot_command"` @@ -167,6 +169,8 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec { "floppy_files": &hcldec.AttrSpec{Name: "floppy_files", Type: cty.List(cty.String), Required: false}, "floppy_dirs": &hcldec.AttrSpec{Name: "floppy_dirs", Type: cty.List(cty.String), Required: false}, "floppy_label": &hcldec.AttrSpec{Name: "floppy_label", Type: cty.String, Required: false}, + "cd_files": &hcldec.AttrSpec{Name: "cd_files", Type: cty.List(cty.String), Required: false}, + "cd_label": &hcldec.AttrSpec{Name: "cd_label", Type: cty.String, Required: false}, "boot_keygroup_interval": &hcldec.AttrSpec{Name: "boot_keygroup_interval", Type: cty.String, Required: false}, "boot_wait": &hcldec.AttrSpec{Name: "boot_wait", Type: cty.String, Required: false}, "boot_command": &hcldec.AttrSpec{Name: "boot_command", Type: cty.List(cty.String), Required: false}, diff --git a/builder/vmware/iso/step_create_vmx.go b/builder/vmware/iso/step_create_vmx.go index 7a837cabd..d73336a9a 100644 --- a/builder/vmware/iso/step_create_vmx.go +++ b/builder/vmware/iso/step_create_vmx.go @@ -34,6 +34,7 @@ type vmxTemplateData struct { DiskType string CDROMType string CDROMType_PrimarySecondary string + CDROM_PATH string Network_Type string Network_Device string @@ -112,6 +113,7 @@ func (s *stepCreateVMX) Run(ctx context.Context, state multistep.StateBag) multi ictx := config.ctx + // Mount extra vmdks we created earlier. if len(config.AdditionalDiskSize) > 0 { for i := range config.AdditionalDiskSize { ictx.Data = &additionalDiskTemplateData{ @@ -238,6 +240,15 @@ func (s *stepCreateVMX) Run(ctx context.Context, state multistep.StateBag) multi return multistep.ActionHalt } + // Add our custom CD, if it exists + cd_path, ok := state.Get("cd_path").(string) + if ok { + if cd_path != "" { + vmxTemplate += ExtraCDRomTemplate + templateData.CDROM_PATH = cd_path + } + } + /// Now that we figured out the CDROM device to add, store it /// to the list of temporary build devices in our statebag tmpBuildDevices := state.Get("temporaryDevices").([]string) @@ -596,3 +607,9 @@ scsi0:{{ .DiskNumber }}.fileName = "{{ .DiskName}}-{{ .DiskNumber }}.vmdk" scsi0:{{ .DiskNumber }}.present = "TRUE" scsi0:{{ .DiskNumber }}.redo = "" ` + +const ExtraCDRomTemplate = ` +{{ .CDROMType }}1:{{ .CDROMType_PrimarySecondary }}.present = "TRUE" +{{ .CDROMType }}1:{{ .CDROMType_PrimarySecondary }}.fileName = "{{ .CDROM_PATH }}" +{{ .CDROMType }}1:{{ .CDROMType_PrimarySecondary }}.deviceType = "cdrom-image" +` diff --git a/common/extra_iso_config.go b/common/extra_iso_config.go new file mode 100644 index 000000000..b870aafce --- /dev/null +++ b/common/extra_iso_config.go @@ -0,0 +1,98 @@ +//go:generate struct-markdown + +package common + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/hashicorp/packer/template/interpolate" +) + +// An iso (CD) containing custom files can be made available for your build. +// +// By default, no extra CD will be attached. All files listed in this setting +// get placed into the root directory of the CD and the CD is attached as the +// second CD device. +// +// This config exists to work around modern operating systems that have no +// way to mount floppy disks, which was our previous go-to for adding files at +// boot time. +type CDConfig struct { + // A list of files to place onto a CD that is attached when the VM is + // booted. This can include either files or directories; any directories + // will be copied onto the CD recursively, preserving directory structure + // hierarchy. Symlinks will have the link's target copied into the directory + // tree on the CD where the symlink was. File globbing is allowed. + // + // Usage example (JSON): + // + // ```json + // "cd_files": ["./somedirectory/meta-data", "./somedirectory/user-data"], + // "cd_label": "cidata", + // ``` + // + // Usage example (HCL): + // + // ```hcl + // cd_files = ["./somedirectory/meta-data", "./somedirectory/user-data"] + // cd_label = "cidata" + // ``` + // + // The above will create a CD with two files, user-data and meta-data in the + // CD root. This specific example is how you would create a CD that can be + // used for an Ubuntu 20.04 autoinstall. + // + // Since globbing is also supported, + // + // ```hcl + // cd_files = ["./somedirectory/*"] + // cd_label = "cidata" + // ``` + // + // Would also be an acceptable way to define the above cd. The difference + // between providing the directory with or without the glob is whether the + // directory itself or its contents will be at the CD root. + // + // Use of this option assums that you have a command line tool isntalled + // that can handle the iso creation. If you are running Packer from an OSX host, + // the required tool is is hdiutil which comes preinstalled. + // On linux hosts, you need to have mkisofs. + // On Windows, you must have oscdimg.exe. oscdimg.exe is part of the + // Windows ADK tooks, downloadable from + // https://docs.microsoft.com/en-us/windows-hardware/get-started/adk-install#winADK + CDFiles []string `mapstructure:"cd_files"` + CDLabel string `mapstructure:"cd_label"` +} + +func (c *CDConfig) Prepare(ctx *interpolate.Context) []error { + var errs []error + var err error + + if c.CDFiles == nil { + c.CDFiles = make([]string, 0) + } + + // Create new file list based on globbing. + var files []string + for _, path := range c.CDFiles { + if strings.ContainsAny(path, "*?[") { + var globbedFiles []string + globbedFiles, err = filepath.Glob(path) + if len(globbedFiles) > 0 { + files = append(files, globbedFiles...) + } + } else { + _, err = os.Stat(path) + files = append(files, path) + } + if err != nil { + errs = append(errs, fmt.Errorf("Bad CD disk file '%s': %s", path, err)) + } + c.CDFiles = files + } + + return errs +} diff --git a/common/extra_iso_config_test.go b/common/extra_iso_config_test.go new file mode 100644 index 000000000..bcc555976 --- /dev/null +++ b/common/extra_iso_config_test.go @@ -0,0 +1,73 @@ +package common + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCDPrepare(t *testing.T) { + type testCases struct { + CDConfig CDConfig + ErrExpected bool + Reason string + ExpectedCDFiles []string + } + tcs := []testCases{ + { + CDConfig: CDConfig{}, + ErrExpected: false, + Reason: "TestNilCD: nil CD array should not fail", + ExpectedCDFiles: []string{}, + }, + { + CDConfig: CDConfig{CDFiles: make([]string, 0)}, + ErrExpected: false, + Reason: "TestEmptyArrayCD: empty CD array should never fail", + ExpectedCDFiles: []string{}, + }, + { + CDConfig: CDConfig{CDFiles: []string{"extra_iso_config.go"}}, + ErrExpected: false, + Reason: "TestExistingCDFile: array with existing CD should not fail", + ExpectedCDFiles: []string{"extra_iso_config.go"}, + }, + { + CDConfig: CDConfig{CDFiles: []string{"does_not_exist.foo"}}, + ErrExpected: true, + Reason: "TestNonExistingCDFile: array with non existing CD should return errors", + ExpectedCDFiles: []string{"does_not_exist.foo"}, + }, + { + CDConfig: CDConfig{CDFiles: []string{"extra_iso_config*"}}, + ErrExpected: false, + Reason: "TestGlobbingCDFile: Glob should work", + ExpectedCDFiles: []string{"extra_iso_config.go", "extra_iso_config_test.go"}, + }, + } + for _, tc := range tcs { + c := tc.CDConfig + errs := c.Prepare(nil) + if (len(errs) != 0) != tc.ErrExpected { + t.Fatal(tc.Reason) + } + assert.Equal(t, c.CDFiles, tc.ExpectedCDFiles) + } +} + +func TestMultiErrorCDFiles(t *testing.T) { + c := CDConfig{ + CDFiles: []string{"extra_iso_config.foo", "extra_iso_config.go", + "extra_iso_config.bar", "extra_iso_config_test.go", "extra_iso_config.baz"}, + } + + errs := c.Prepare(nil) + if len(errs) == 0 { + t.Fatal("array with non existing CD should return errors") + } + + expectedErrors := 3 + if count := len(errs); count != expectedErrors { + t.Fatalf("array with %v non existing CD should return %v errors but it is returning %v", expectedErrors, expectedErrors, count) + } +} diff --git a/common/step_create_cdrom.go b/common/step_create_cdrom.go new file mode 100644 index 000000000..bfbd5e748 --- /dev/null +++ b/common/step_create_cdrom.go @@ -0,0 +1,201 @@ +package common + +import ( + "context" + "fmt" + "io" + "log" + "os" + "os/exec" + "path/filepath" + "runtime" + + "github.com/hashicorp/packer/helper/builder/localexec" + "github.com/hashicorp/packer/helper/multistep" + "github.com/hashicorp/packer/packer" + "github.com/hashicorp/packer/packer/tmp" +) + +// StepCreateCD will create a CD disk with the given files. +type StepCreateCD struct { + // Files can be either files or directories. Any files provided here will + // be written to the root of the CD. Directories will be written to the + // root of the CD as well, but will retain their subdirectory structure. + Files []string + Label string + + CDPath string + + filesAdded map[string]bool +} + +func (s *StepCreateCD) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { + if len(s.Files) == 0 { + log.Println("No CD files specified. CD disk will not be made.") + return multistep.ActionContinue + } + + ui := state.Get("ui").(packer.Ui) + ui.Say("Creating CD disk...") + + if s.Label == "" { + s.Label = "packer" + } else { + log.Printf("CD label is set to %s", s.Label) + } + + // Track what files are added. Used for testing step. + s.filesAdded = make(map[string]bool) + + // Create a temporary file to be our CD drive + CDF, err := tmp.File("packer*.iso") + // Set the path so we can remove it later + CDPath := CDF.Name() + CDF.Close() + os.Remove(CDPath) + if err != nil { + state.Put("error", + fmt.Errorf("Error creating temporary file for CD: %s", err)) + return multistep.ActionHalt + } + + log.Printf("CD path: %s", CDPath) + s.CDPath = CDPath + + // Consolidate all files provided into a single directory to become our + // "root" directory. + rootFolder, err := tmp.Dir("packer_to_cdrom") + if err != nil { + state.Put("error", + fmt.Errorf("Error creating temporary file for CD: %s", err)) + return multistep.ActionHalt + } + + for _, toAdd := range s.Files { + err = s.AddFile(rootFolder, toAdd) + if err != nil { + state.Put("error", + fmt.Errorf("Error creating temporary file for CD: %s", err)) + return multistep.ActionHalt + } + } + + cmd := retrieveCommandForOS(s.Label, rootFolder, CDPath) + err = localexec.RunAndStream(cmd, ui, []string{}) + if err != nil { + state.Put("error", err) + return multistep.ActionHalt + } + + ui.Message("Done copying paths from CD_dirs") + + // Set the path to the CD so it can be used later + state.Put("cd_path", CDPath) + + if err != nil { + state.Put("error", err) + return multistep.ActionHalt + } + + return multistep.ActionContinue +} + +func (s *StepCreateCD) Cleanup(multistep.StateBag) { + if s.CDPath != "" { + log.Printf("Deleting CD disk: %s", s.CDPath) + os.Remove(s.CDPath) + } +} + +func retrieveCommandForOS(label string, source string, dest string) *exec.Cmd { + switch runtime.GOOS { + case "windows": + cmd := exec.Command("oscdimg") + args := []string{"-j1", "-o", "-m", "-l" + label, source, dest} + cmd.Args = append(cmd.Args, args...) + return cmd + case "darwin": + cmd := exec.Command("hdiutil") + args := []string{"makehybrid", "-o", dest, "-hfs", + "-joliet", "-iso", "-default-volume-name", label, source} + cmd.Args = append(cmd.Args, args...) + return cmd + default: + cmd := exec.Command("mkisofs") + args := []string{"-joliet", "-volid", label, "-o", dest, source} + cmd.Args = append(cmd.Args, args...) + return cmd + } +} + +func (s *StepCreateCD) AddFile(dst, src string) error { + finfo, err := os.Stat(src) + if err != nil { + return fmt.Errorf("Error adding path to CD: %s", err) + } + + // add a file + if !finfo.IsDir() { + inputF, err := os.Open(src) + if err != nil { + return err + } + defer inputF.Close() + + // Create a new file in the root directory + dest, err := os.Create(filepath.Join(dst, finfo.Name())) + if err != nil { + return fmt.Errorf("Error opening file for copy %s to CD root", src) + } + defer dest.Close() + nBytes, err := io.Copy(dest, inputF) + if err != nil { + return fmt.Errorf("Error copying %s to CD root", src) + } + s.filesAdded[src] = true + log.Printf("Wrote %d bytes to %s", nBytes, finfo.Name()) + return err + } + + // Add a directory and its subdirectories + visit := func(pathname string, fi os.FileInfo, err error) error { + if err != nil { + return err + } + + // add a file + if !fi.IsDir() { + inputF, err := os.Open(pathname) + if err != nil { + return err + } + defer inputF.Close() + + fileDst, err := os.Create(filepath.Join(dst, pathname)) + if err != nil { + return fmt.Errorf("Error opening file %s on CD", src) + } + defer fileDst.Close() + nBytes, err := io.Copy(fileDst, inputF) + if err != nil { + return fmt.Errorf("Error copying %s to CD", src) + } + s.filesAdded[pathname] = true + log.Printf("Wrote %d bytes to %s", nBytes, pathname) + return err + } + + if fi.Mode().IsDir() { + // create the directory on the CD, continue walk. + err := os.Mkdir(filepath.Join(dst, pathname), fi.Mode()) + if err != nil { + err = fmt.Errorf("error creating new directory %s: %s", + filepath.Join(dst, pathname), err) + } + return err + } + return err + } + + return filepath.Walk(src, visit) +} diff --git a/common/step_create_cdrom_test.go b/common/step_create_cdrom_test.go new file mode 100644 index 000000000..bdcc83535 --- /dev/null +++ b/common/step_create_cdrom_test.go @@ -0,0 +1,119 @@ +package common + +import ( + "bytes" + "context" + "io/ioutil" + "os" + "path" + "testing" + + "github.com/hashicorp/packer/helper/multistep" + "github.com/hashicorp/packer/packer" +) + +func TestStepCreateCD_Impl(t *testing.T) { + var raw interface{} + raw = new(StepCreateCD) + if _, ok := raw.(multistep.Step); !ok { + t.Fatalf("StepCreateCD should be a step") + } +} + +func testStepCreateCDState(t *testing.T) multistep.StateBag { + state := new(multistep.BasicStateBag) + state.Put("ui", &packer.BasicUi{ + Reader: new(bytes.Buffer), + Writer: new(bytes.Buffer), + }) + return state +} + +func TestStepCreateCD(t *testing.T) { + if os.Getenv("PACKER_ACC") == "" { + t.Skip("This test is only run with PACKER_ACC=1 due to the requirement of access to the disk management binaries.") + } + state := testStepCreateCDState(t) + step := new(StepCreateCD) + + dir, err := ioutil.TempDir("", "packer") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.RemoveAll(dir) + + files := make([]string, 3) + + tempFileNames := []string{"test_cd_roms.tmp", "test cd files.tmp", + "Test-Test-Test5.tmp"} + for i, fname := range tempFileNames { + files[i] = path.Join(dir, fname) + + _, err := os.Create(files[i]) + if err != nil { + t.Fatalf("err: %s", err) + } + } + + step.Files = files + action := step.Run(context.Background(), state) + + if err, ok := state.GetOk("error"); ok { + t.Fatalf("state should be ok for %v: %s", step.Files, err) + } + + if action != multistep.ActionContinue { + t.Fatalf("bad action: %#v for %v", action, step.Files) + } + + CD_path := state.Get("cd_path").(string) + + if _, err := os.Stat(CD_path); err != nil { + t.Fatalf("file not found: %s for %v", CD_path, step.Files) + } + + if len(step.filesAdded) != 3 { + t.Fatalf("expected 3 files, found %d for %v", len(step.filesAdded), step.Files) + } + + step.Cleanup(state) + + if _, err := os.Stat(CD_path); err == nil { + t.Fatalf("file found: %s for %v", CD_path, step.Files) + } +} + +func TestStepCreateCD_missing(t *testing.T) { + if os.Getenv("PACKER_ACC") == "" { + t.Skip("This test is only run with PACKER_ACC=1 due to the requirement of access to the disk management binaries.") + } + state := testStepCreateCDState(t) + step := new(StepCreateCD) + + dir, err := ioutil.TempDir("", "packer") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.RemoveAll(dir) + + expected := 0 + + step.Files = []string{"missing file.tmp"} + if action := step.Run(context.Background(), state); action != multistep.ActionHalt { + t.Fatalf("bad action: %#v for %v", action, step.Files) + } + + if _, ok := state.GetOk("error"); !ok { + t.Fatalf("state should not be ok for %v", step.Files) + } + + CD_path := state.Get("cd_path") + + if CD_path != nil { + t.Fatalf("CD_path is not nil for %v", step.Files) + } + + if len(step.filesAdded) != expected { + t.Fatalf("expected %d, found %d for %v", expected, len(step.filesAdded), step.Files) + } +} diff --git a/go.sum b/go.sum index 823925a32..6c12cf677 100644 --- a/go.sum +++ b/go.sum @@ -159,6 +159,8 @@ github.com/digitalocean/godo v1.11.1 h1:OsTh37YFKk+g6DnAOrkXJ9oDArTkRx5UTkBJ2EWA github.com/digitalocean/godo v1.11.1/go.mod h1:h6faOIcZ8lWIwNQ+DN7b3CgX4Kwby5T+nbpNqkUIozU= github.com/dimchansky/utfbom v1.1.0 h1:FcM3g+nofKgUteL8dm/UpdRXNC9KmADgTpLKsu0TRo4= github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8= +github.com/diskfs/go-diskfs v1.1.1 h1:rMjLpaydtXGVZb7mdkRGK1+//30i76nKAit89zUzeaI= +github.com/diskfs/go-diskfs v1.1.1/go.mod h1:afUPxxu+x1snp4aCY2bKR0CoZ/YFJewV3X2UEr2nPZE= github.com/dnaeon/go-vcr v1.0.1 h1:r8L/HqC0Hje5AXMu1ooW8oyQyOFv4GxqpL0nRP7SLLY= github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= github.com/docker/docker v0.0.0-20180422163414-57142e89befe h1:VW8TnWi0CZgg7oCv0wH6evNwkzcJg/emnw4HrVIWws4= @@ -362,8 +364,6 @@ github.com/hashicorp/golang-lru v0.5.3 h1:YPkqC67at8FYaadspW/6uE0COsBxS2656RLEr8 github.com/hashicorp/golang-lru v0.5.3/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/hcl/v2 v2.3.0 h1:iRly8YaMwTBAKhn1Ybk7VSdzbnopghktCD031P8ggUE= -github.com/hashicorp/hcl/v2 v2.3.0/go.mod h1:d+FwDBbOLvpAM3Z6J7gPj/VoAGkNe/gm352ZhjJ/Zv8= github.com/hashicorp/hcl/v2 v2.6.0 h1:3krZOfGY6SziUXa6H9PJU6TyohHn7I+ARYnhbeNBz+o= github.com/hashicorp/hcl/v2 v2.6.0/go.mod h1:bQTN5mpo+jewjJgh8jr0JUguIi7qPHUF6yIfAEN3jqY= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= @@ -761,6 +761,7 @@ golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1 h1:ogLJMz+qpzav7lGMh10LMvAkM/fAoGlaiiHYiFYdm80= golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -881,6 +882,8 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogR gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/cheggaaa/pb.v1 v1.0.27 h1:kJdccidYzt3CaHD1crCFTS1hxyhSi059NhOFUf03YFo= gopkg.in/cheggaaa/pb.v1 v1.0.27/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/djherbis/times.v1 v1.2.0 h1:UCvDKl1L/fmBygl2Y7hubXCnY7t4Yj46ZrBFNUipFbM= +gopkg.in/djherbis/times.v1 v1.2.0/go.mod h1:AQlg6unIsrsCEdQYhTzERy542dz6SFdQFZFv6mUY0P8= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.42.0 h1:7N3gPTt50s8GuLortA00n8AqRTk75qOP98+mTPpgzRk= gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= diff --git a/website/pages/docs/builders/hyperv/iso.mdx b/website/pages/docs/builders/hyperv/iso.mdx index d5c7eef18..078f6d3fd 100644 --- a/website/pages/docs/builders/hyperv/iso.mdx +++ b/website/pages/docs/builders/hyperv/iso.mdx @@ -105,6 +105,10 @@ builder. @include 'common/FloppyConfig-not-required.mdx' +### CD configuration reference + +@include 'common/CDConfig-not-required.mdx' + ## Communicator configuration reference ### Optional common fields: @@ -136,8 +140,8 @@ for the version of Hyper-V that is running. Floppy drives are no longer supported by generation 2 machines. This requires you to take another approach when dealing with preseed or answer files. Two -possible options are using virtual DVD drives or using Packers built in web -server. +possible options are using your own virtual DVD drives, or the cd_files option, +or using Packer's built in web server. When dealing with Windows you need to enable UEFI drives for generation 2 virtual machines. diff --git a/website/pages/docs/builders/vmware/iso.mdx b/website/pages/docs/builders/vmware/iso.mdx index 12b447add..fed7d1bc1 100644 --- a/website/pages/docs/builders/vmware/iso.mdx +++ b/website/pages/docs/builders/vmware/iso.mdx @@ -78,6 +78,7 @@ the items listed here, you will want to look at the general configuration references for [ISO](#iso-configuration), [HTTP](#http-directory-configuration), [Floppy](#floppy-configuration), +[CD](#cd-configuration), [Boot](#boot-configuration), [Driver](#driver-configuration), [Hardware](#hardware-configuration), @@ -129,6 +130,9 @@ necessary for this build to succeed and can be found further down the page. @include 'common/FloppyConfig-not-required.mdx' +### CD configuration +@include 'common/CDConfig-not-required.mdx' + ### Shutdown configuration #### Optional: diff --git a/website/pages/partials/common/CDConfig-not-required.mdx b/website/pages/partials/common/CDConfig-not-required.mdx new file mode 100644 index 000000000..4ce0e860c --- /dev/null +++ b/website/pages/partials/common/CDConfig-not-required.mdx @@ -0,0 +1,46 @@ + + +- `cd_files` ([]string) - A list of files to place onto a CD that is attached when the VM is + booted. This can include either files or directories; any directories + will be copied onto the CD recursively, preserving directory structure + hierarchy. Symlinks will have the link's target copied into the directory + tree on the CD where the symlink was. File globbing is allowed. + + Usage example (JSON): + + ```json + "cd_files": ["./somedirectory/meta-data", "./somedirectory/user-data"], + "cd_label": "cidata", + ``` + + Usage example (HCL): + + ```hcl + cd_files = ["./somedirectory/meta-data", "./somedirectory/user-data"] + cd_label = "cidata" + ``` + + The above will create a CD with two files, user-data and meta-data in the + CD root. This specific example is how you would create a CD that can be + used for an Ubuntu 20.04 autoinstall. + + Since globbing is also supported, + + ```hcl + cd_files = ["./somedirectory/*"] + cd_label = "cidata" + ``` + + Would also be an acceptable way to define the above cd. The difference + between providing the directory with or without the glob is whether the + directory itself or its contents will be at the CD root. + + Use of this option assums that you have a command line tool isntalled + that can handle the iso creation. If you are running Packer from an OSX host, + the required tool is is hdiutil which comes preinstalled. + On linux hosts, you need to have mkisofs. + On Windows, you must have oscdimg.exe. oscdimg.exe is part of the + Windows ADK tooks, downloadable from + https://docs.microsoft.com/en-us/windows-hardware/get-started/adk-install#winADK + +- `cd_label` (string) - CD Label diff --git a/website/pages/partials/common/CDConfig.mdx b/website/pages/partials/common/CDConfig.mdx new file mode 100644 index 000000000..b234df3eb --- /dev/null +++ b/website/pages/partials/common/CDConfig.mdx @@ -0,0 +1,11 @@ + + +An iso (CD) containing custom files can be made available for your build. + +By default, no extra CD will be attached. All files listed in this setting +get placed into the root directory of the CD and the CD is attached as the +second CD device. + +This config exists to work around modern operating systems that have no +way to mount floppy disks, which was our previous go-to for adding files at +boot time. From 900100f591415d0dc2967804fa92bc4a8af9e869 Mon Sep 17 00:00:00 2001 From: Megan Marsh Date: Tue, 8 Sep 2020 11:17:20 -0700 Subject: [PATCH 2/4] Update website/pages/docs/builders/hyperv/iso.mdx Co-authored-by: Wilken Rivera --- website/pages/docs/builders/hyperv/iso.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/pages/docs/builders/hyperv/iso.mdx b/website/pages/docs/builders/hyperv/iso.mdx index 078f6d3fd..3606fcecc 100644 --- a/website/pages/docs/builders/hyperv/iso.mdx +++ b/website/pages/docs/builders/hyperv/iso.mdx @@ -140,7 +140,7 @@ for the version of Hyper-V that is running. Floppy drives are no longer supported by generation 2 machines. This requires you to take another approach when dealing with preseed or answer files. Two -possible options are using your own virtual DVD drives, or the cd_files option, +possible options are using your own virtual DVD drives, the cd_files option, or using Packer's built in web server. When dealing with Windows you need to enable UEFI drives for generation 2 @@ -856,4 +856,4 @@ is actually installed and ready to be connected to. For more information about the hyper-v daemons and supported distributions, see the Microsoft docs at -https://docs.microsoft.com/en-us/windows-server/virtualization/hyper-v/supported-linux-and-freebsd-virtual-machines-for-hyper-v-on-windows \ No newline at end of file +https://docs.microsoft.com/en-us/windows-server/virtualization/hyper-v/supported-linux-and-freebsd-virtual-machines-for-hyper-v-on-windows From 988ab667310ca90824c2cf81a3d61ab5630c567c Mon Sep 17 00:00:00 2001 From: Megan Marsh Date: Tue, 8 Sep 2020 11:18:10 -0700 Subject: [PATCH 3/4] Update common/extra_iso_config.go thanks for the copyediting :D Co-authored-by: Wilken Rivera --- common/extra_iso_config.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/extra_iso_config.go b/common/extra_iso_config.go index b870aafce..8ba5c58ea 100644 --- a/common/extra_iso_config.go +++ b/common/extra_iso_config.go @@ -56,9 +56,9 @@ type CDConfig struct { // between providing the directory with or without the glob is whether the // directory itself or its contents will be at the CD root. // - // Use of this option assums that you have a command line tool isntalled + // Use of this option assumes that you have a command line tool installed // that can handle the iso creation. If you are running Packer from an OSX host, - // the required tool is is hdiutil which comes preinstalled. + // the required tool is hdiutil which comes preinstalled. // On linux hosts, you need to have mkisofs. // On Windows, you must have oscdimg.exe. oscdimg.exe is part of the // Windows ADK tooks, downloadable from From 40c128767efbad60137344c62afad3c2c57aa2ba Mon Sep 17 00:00:00 2001 From: Megan Marsh Date: Tue, 8 Sep 2020 12:17:58 -0700 Subject: [PATCH 4/4] regenerate docs --- website/pages/partials/common/CDConfig-not-required.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/pages/partials/common/CDConfig-not-required.mdx b/website/pages/partials/common/CDConfig-not-required.mdx index 4ce0e860c..adcf0a1bd 100644 --- a/website/pages/partials/common/CDConfig-not-required.mdx +++ b/website/pages/partials/common/CDConfig-not-required.mdx @@ -35,9 +35,9 @@ between providing the directory with or without the glob is whether the directory itself or its contents will be at the CD root. - Use of this option assums that you have a command line tool isntalled + Use of this option assumes that you have a command line tool installed that can handle the iso creation. If you are running Packer from an OSX host, - the required tool is is hdiutil which comes preinstalled. + the required tool is hdiutil which comes preinstalled. On linux hosts, you need to have mkisofs. On Windows, you must have oscdimg.exe. oscdimg.exe is part of the Windows ADK tooks, downloadable from