diff --git a/builder/azure/chroot/builder.go b/builder/azure/chroot/builder.go index ec4181fb8..4617ebe2f 100644 --- a/builder/azure/chroot/builder.go +++ b/builder/azure/chroot/builder.go @@ -391,8 +391,11 @@ func checkHyperVGeneration(s string) interface{} { } func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (packer.Artifact, error) { - if runtime.GOOS != "linux" { - return nil, errors.New("the azure-chroot builder only works on Linux environments") + switch runtime.GOOS { + case "linux", "freebsd": + break + default: + return nil, errors.New("the azure-chroot builder only works on Linux and FreeBSD environments") } err := b.config.ClientConfig.FillParameters() diff --git a/builder/azure/chroot/diskattacher.go b/builder/azure/chroot/diskattacher.go index 6440491b7..a6516e3e3 100644 --- a/builder/azure/chroot/diskattacher.go +++ b/builder/azure/chroot/diskattacher.go @@ -3,24 +3,18 @@ package chroot import ( "context" "errors" - "fmt" "log" - "os" - "path/filepath" "strings" - "syscall" "time" - "github.com/hashicorp/packer/builder/azure/common/client" - "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-12-01/compute" "github.com/Azure/go-autorest/autorest/azure" "github.com/Azure/go-autorest/autorest/to" + "github.com/hashicorp/packer/builder/azure/common/client" ) type DiskAttacher interface { AttachDisk(ctx context.Context, disk string) (lun int32, err error) - DiskPathForLun(lun int32) string WaitForDevice(ctx context.Context, i int32) (device string, err error) DetachDisk(ctx context.Context, disk string) (err error) WaitForDetach(ctx context.Context, diskID string) error @@ -38,31 +32,7 @@ type diskAttacher struct { vm *client.ComputeInfo // store info about this VM so that we don't have to ask metadata service on every call } -func (diskAttacher) DiskPathForLun(lun int32) string { - return fmt.Sprintf("/dev/disk/azure/scsi1/lun%d", lun) -} - -func (da diskAttacher) WaitForDevice(ctx context.Context, lun int32) (device string, err error) { - path := da.DiskPathForLun(lun) - - for { - link, err := os.Readlink(path) - if err == nil { - return filepath.Abs("/dev/disk/azure/scsi1/" + link) - } else if err != os.ErrNotExist { - if pe, ok := err.(*os.PathError); ok && pe.Err != syscall.ENOENT { - return "", err - } - } - - select { - case <-time.After(100 * time.Millisecond): - // continue - case <-ctx.Done(): - return "", ctx.Err() - } - } -} +var DiskNotFoundError = errors.New("Disk not found") func (da *diskAttacher) DetachDisk(ctx context.Context, diskID string) error { log.Println("Fetching list of disks currently attached to VM") @@ -111,8 +81,6 @@ func (da *diskAttacher) WaitForDetach(ctx context.Context, diskID string) error } } -var DiskNotFoundError = errors.New("Disk not found") - func (da *diskAttacher) AttachDisk(ctx context.Context, diskID string) (int32, error) { dataDisks, err := da.getDisks(ctx) if err != nil { diff --git a/builder/azure/chroot/diskattacher_freebsd.go b/builder/azure/chroot/diskattacher_freebsd.go new file mode 100644 index 000000000..ecfc5cf60 --- /dev/null +++ b/builder/azure/chroot/diskattacher_freebsd.go @@ -0,0 +1,54 @@ +package chroot + +import ( + "bufio" + "bytes" + "context" + "fmt" + "os/exec" + "regexp" + "strings" + "time" +) + +func (da diskAttacher) WaitForDevice(ctx context.Context, lun int32) (device string, err error) { + // This builder will always be running in Azure, where data disks show up + // on scbus5 target 0. The camcontrol command always outputs LUNs in + // unpadded hexadecimal format. + regexStr := fmt.Sprintf(`at scbus5 target 0 lun %x \(.*?da([\d]+)`, lun) + devRegex, err := regexp.Compile(regexStr) + if err != nil { + return "", err + } + for { + cmd := exec.Command("camcontrol", "devlist") + var out bytes.Buffer + cmd.Stdout = &out + err = cmd.Run() + if err != nil { + return "", err + } + outString := out.String() + scanner := bufio.NewScanner(strings.NewReader(outString)) + for scanner.Scan() { + line := scanner.Text() + // Check if this is the correct bus, target, and LUN. + if matches := devRegex.FindStringSubmatch(line); matches != nil { + // If this function immediately returns, devfs won't have + // created the device yet. + time.Sleep(1000 * time.Millisecond) + return fmt.Sprintf("/dev/da%s", matches[1]), nil + } + } + if err = scanner.Err(); err != nil { + return "", err + } + + select { + case <-time.After(100 * time.Millisecond): + // continue + case <-ctx.Done(): + return "", ctx.Err() + } + } +} diff --git a/builder/azure/chroot/diskattacher_linux.go b/builder/azure/chroot/diskattacher_linux.go new file mode 100644 index 000000000..3d1403691 --- /dev/null +++ b/builder/azure/chroot/diskattacher_linux.go @@ -0,0 +1,36 @@ +package chroot + +import ( + "context" + "fmt" + "os" + "path/filepath" + "syscall" + "time" +) + +func diskPathForLun(lun int32) string { + return fmt.Sprintf("/dev/disk/azure/scsi1/lun%d", lun) +} + +func (da diskAttacher) WaitForDevice(ctx context.Context, lun int32) (device string, err error) { + path := diskPathForLun(lun) + + for { + link, err := os.Readlink(path) + if err == nil { + return filepath.Abs("/dev/disk/azure/scsi1/" + link) + } else if err != os.ErrNotExist { + if pe, ok := err.(*os.PathError); ok && pe.Err != syscall.ENOENT { + return "", err + } + } + + select { + case <-time.After(100 * time.Millisecond): + // continue + case <-ctx.Done(): + return "", ctx.Err() + } + } +} diff --git a/builder/azure/chroot/diskattacher_other.go b/builder/azure/chroot/diskattacher_other.go new file mode 100644 index 000000000..3fea09037 --- /dev/null +++ b/builder/azure/chroot/diskattacher_other.go @@ -0,0 +1,11 @@ +// +build !linux,!freebsd + +package chroot + +import ( + "context" +) + +func (da diskAttacher) WaitForDevice(ctx context.Context, lun int32) (device string, err error) { + panic("The azure-chroot builder does not work on this platform.") +} diff --git a/builder/azure/chroot/step_mount_device.go b/builder/azure/chroot/step_mount_device.go index e4fc02595..99fbb9d90 100644 --- a/builder/azure/chroot/step_mount_device.go +++ b/builder/azure/chroot/step_mount_device.go @@ -9,6 +9,7 @@ import ( "log" "os" "path/filepath" + "runtime" "strings" "github.com/hashicorp/packer/common" @@ -62,7 +63,13 @@ func (s *StepMountDevice) Run(ctx context.Context, state multistep.StateBag) mul return multistep.ActionHalt } - deviceMount := fmt.Sprintf("%s%s", device, s.MountPartition) + var deviceMount string + switch runtime.GOOS { + case "freebsd": + deviceMount = fmt.Sprintf("%sp%s", device, s.MountPartition) + default: + deviceMount = fmt.Sprintf("%s%s", device, s.MountPartition) + } state.Put("deviceMount", deviceMount) diff --git a/builder/azure/chroot/step_mount_device_test.go b/builder/azure/chroot/step_mount_device_test.go new file mode 100644 index 000000000..98ed49bc7 --- /dev/null +++ b/builder/azure/chroot/step_mount_device_test.go @@ -0,0 +1,72 @@ +package chroot + +import ( + "context" + "fmt" + "io/ioutil" + "os" + "runtime" + "testing" + "time" + + "github.com/hashicorp/packer/common" + "github.com/hashicorp/packer/helper/multistep" +) + +func TestStepMountDevice_Run(t *testing.T) { + switch runtime.GOOS { + case "linux", "freebsd": + break + default: + t.Skip("Unsupported operating system") + } + mountPath, err := ioutil.TempDir("", "stepmountdevicetest") + if err != nil { + t.Errorf("Unable to create a temporary directory: %q", err) + } + step := &StepMountDevice{ + MountOptions: []string{"foo"}, + MountPartition: "42", + MountPath: mountPath, + } + + var gotCommand string + var wrapper common.CommandWrapper + wrapper = func(ran string) (string, error) { + gotCommand = ran + return "", nil + } + + state := new(multistep.BasicStateBag) + state.Put("wrappedCommand", wrapper) + state.Put("device", "/dev/quux") + + ui, getErrs := testUI() + state.Put("ui", ui) + + var config Config + state.Put("config", &config) + + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + got := step.Run(ctx, state) + if got != multistep.ActionContinue { + t.Errorf("Expected 'continue', but got '%v'", got) + } + + var expectedMountDevice string + switch runtime.GOOS { + case "freebsd": + expectedMountDevice = "/dev/quuxp42" + default: // currently just Linux + expectedMountDevice = "/dev/quux42" + } + expectedCommand := fmt.Sprintf("mount -o foo %s %s", expectedMountDevice, mountPath) + if gotCommand != expectedCommand { + t.Errorf("Expected '%v', but got '%v'", expectedCommand, gotCommand) + } + + os.Remove(mountPath) + _ = getErrs +} diff --git a/common/chroot/step_copy_files.go b/common/chroot/step_copy_files.go index 496578bfc..ecf011b38 100644 --- a/common/chroot/step_copy_files.go +++ b/common/chroot/step_copy_files.go @@ -6,6 +6,7 @@ import ( "fmt" "log" "path/filepath" + "runtime" "github.com/hashicorp/packer/common" "github.com/hashicorp/packer/helper/multistep" @@ -31,12 +32,22 @@ func (s *StepCopyFiles) Run(ctx context.Context, state multistep.StateBag) multi s.files = make([]string, 0, len(s.Files)) if len(s.Files) > 0 { ui.Say("Copying files from host to chroot...") + var removeDestinationOption string + switch runtime.GOOS { + case "freebsd": + // The -f option here is closer to GNU --remove-destination than + // what POSIX says -f should do. + removeDestinationOption = "-f" + default: + // This is the GNU binutils version. + removeDestinationOption = "--remove-destination" + } for _, path := range s.Files { ui.Message(path) chrootPath := filepath.Join(mountPath, path) log.Printf("Copying '%s' to '%s'", path, chrootPath) - cmdText, err := wrappedCommand(fmt.Sprintf("cp --remove-destination %s %s", path, chrootPath)) + cmdText, err := wrappedCommand(fmt.Sprintf("cp %s %s %s", removeDestinationOption, path, chrootPath)) if err != nil { err := fmt.Errorf("Error building copy command: %s", err) state.Put("error", err) diff --git a/common/chroot/step_copy_files_test.go b/common/chroot/step_copy_files_test.go index 281613e6f..381f7ab82 100644 --- a/common/chroot/step_copy_files_test.go +++ b/common/chroot/step_copy_files_test.go @@ -1,6 +1,30 @@ package chroot -import "testing" +import ( + "context" + "fmt" + "io/ioutil" + "path" + "runtime" + "strings" + "testing" + "time" + + "github.com/hashicorp/packer/common" + "github.com/hashicorp/packer/helper/multistep" + "github.com/hashicorp/packer/packer" +) + +// testUI returns a test ui plus a function to retrieve the errors written to the ui +func testUI() (packer.Ui, func() string) { + errorBuffer := &strings.Builder{} + ui := &packer.BasicUi{ + Reader: strings.NewReader(""), + Writer: ioutil.Discard, + ErrorWriter: errorBuffer, + } + return ui, errorBuffer.String +} func TestCopyFilesCleanupFunc_ImplementsCleanupFunc(t *testing.T) { var raw interface{} @@ -9,3 +33,95 @@ func TestCopyFilesCleanupFunc_ImplementsCleanupFunc(t *testing.T) { t.Fatalf("cleanup func should be a CleanupFunc") } } + +func TestCopyFiles_Run(t *testing.T) { + mountPath := "/mnt/abcde" + copySource := "/etc/resolv.conf" + copyDestination := path.Join(mountPath, "etc", "resolv.conf") + + step := &StepCopyFiles{ + Files: []string{ + copySource, + }, + } + + var gotCommand string + commandRunCount := 0 + var wrapper common.CommandWrapper + wrapper = func(ran string) (string, error) { + gotCommand = ran + commandRunCount++ + return "", nil + } + + state := new(multistep.BasicStateBag) + state.Put("mount_path", mountPath) + state.Put("wrappedCommand", wrapper) + + ui, getErrs := testUI() + state.Put("ui", ui) + + var expectedCopyTemplate string + + switch runtime.GOOS { + case "linux": + expectedCopyTemplate = "cp --remove-destination %s %s" + case "freebsd": + expectedCopyTemplate = "cp -f %s %s" + default: + t.Skip("Unsupported operating system") + } + + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + got := step.Run(ctx, state) + if got != multistep.ActionContinue { + t.Errorf("Expected 'continue', but got '%v'", got) + } + + if commandRunCount != 1 { + t.Errorf("Copy command should run exactly once but ran %v times", commandRunCount) + } + + expectedCopyCommand := fmt.Sprintf(expectedCopyTemplate, copySource, copyDestination) + if gotCommand != expectedCopyCommand { + t.Errorf("Expected command was '%v' but actual was '%v'", expectedCopyCommand, gotCommand) + } + + _ = getErrs +} + +func TestCopyFiles_CopyNothing(t *testing.T) { + step := &StepCopyFiles{ + Files: []string{}, + } + + commandRunCount := 0 + var wrapper common.CommandWrapper + wrapper = func(ran string) (string, error) { + commandRunCount++ + return "", nil + } + + state := new(multistep.BasicStateBag) + state.Put("mount_path", "/mnt/something") + state.Put("wrappedCommand", wrapper) + + ui, getErrs := testUI() + state.Put("ui", ui) + + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + got := step.Run(ctx, state) + if got != multistep.ActionContinue { + t.Errorf("Expected 'continue', but got '%v'", got) + } + + if commandRunCount != 0 { + t.Errorf("Copy command should not run but ran %v times", commandRunCount) + } + + _ = getErrs +} diff --git a/examples/azure/freebsd-chroot.json b/examples/azure/freebsd-chroot.json new file mode 100644 index 000000000..8b8d42825 --- /dev/null +++ b/examples/azure/freebsd-chroot.json @@ -0,0 +1,26 @@ +{ + "variables": {}, + "builders": [ + { + "type": "azure-chroot", + "source": "thefreebsdfoundation:freebsd-12_1:12_1-release:latest", + "image_resource_id": "/subscriptions/{{vm `subscription_id`}}/resourceGroups/{{vm `resource_group`}}/providers/Microsoft.Compute/images/freebsd-{{timestamp}}", + "os_disk_size_gb": 64, + "os_disk_storage_account_type": "Premium_LRS", + "mount_partition": 2, + "chroot_mounts": [ + ["devfs", "devfs", "/dev"], + ["procfs", "procfs", "/proc"] + ] + } + ], + "provisioners": [ + { + "inline": [ + "env ASSUME_ALWAYS_YES=YES pkg bootstrap" + ], + "inline_shebang": "/bin/sh -x", + "type": "shell" + } + ] +}