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.