Merge pull request #9697 from backerman/personal/backerman/9695

Add FreeBSD support to azure/chroot
This commit is contained in:
Megan Marsh 2020-08-10 12:47:48 -07:00 committed by GitHub
commit f8bb9008c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 343 additions and 39 deletions

View File

@ -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()

View File

@ -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 {

View File

@ -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()
}
}
}

View File

@ -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()
}
}
}

View File

@ -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.")
}

View File

@ -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)

View File

@ -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
}

View File

@ -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)

View File

@ -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
}

View File

@ -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"
}
]
}