From bd4ce90728b1dda122f8e1cf4e77c0771ae5bbf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Sm=C3=B3=C5=82ka?= Date: Mon, 28 Jan 2019 12:30:15 +0100 Subject: [PATCH] Add chroot disk build --- builder/hyperone/builder.go | 37 ++++++++- builder/hyperone/chroot_communicator.go | 54 ++++++++++++ builder/hyperone/config.go | 70 ++++++++++++++++ builder/hyperone/step_chroot_provision.go | 28 +++++++ builder/hyperone/step_copy_files.go | 40 +++++++++ builder/hyperone/step_create_vm.go | 40 ++++++--- builder/hyperone/step_create_vm_from_disk.go | 70 ++++++++++++++++ builder/hyperone/step_detach_disk.go | 34 ++++++++ builder/hyperone/step_mount_chroot.go | 51 ++++++++++++ builder/hyperone/step_mount_extra.go | 45 ++++++++++ builder/hyperone/step_post_mount_commands.go | 37 +++++++++ builder/hyperone/step_pre_mount_commands.go | 37 +++++++++ builder/hyperone/step_prepare_device.go | 46 +++++++++++ builder/hyperone/utils.go | 40 +++++++++ examples/hyperone/chroot.json | 29 +++++++ website/source/docs/builders/hyperone.html.md | 82 +++++++++++++++++-- 16 files changed, 720 insertions(+), 20 deletions(-) create mode 100644 builder/hyperone/chroot_communicator.go create mode 100644 builder/hyperone/step_chroot_provision.go create mode 100644 builder/hyperone/step_copy_files.go create mode 100644 builder/hyperone/step_create_vm_from_disk.go create mode 100644 builder/hyperone/step_detach_disk.go create mode 100644 builder/hyperone/step_mount_chroot.go create mode 100644 builder/hyperone/step_mount_extra.go create mode 100644 builder/hyperone/step_post_mount_commands.go create mode 100644 builder/hyperone/step_pre_mount_commands.go create mode 100644 builder/hyperone/step_prepare_device.go create mode 100644 examples/hyperone/chroot.json diff --git a/builder/hyperone/builder.go b/builder/hyperone/builder.go index 8858d01eb..2c8b1d290 100644 --- a/builder/hyperone/builder.go +++ b/builder/hyperone/builder.go @@ -8,6 +8,7 @@ import ( "github.com/hashicorp/packer/helper/communicator" "github.com/hashicorp/packer/helper/multistep" "github.com/hashicorp/packer/packer" + "github.com/hashicorp/packer/template/interpolate" "github.com/hyperonecom/h1-client-go" ) @@ -41,12 +42,23 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { return nil, nil } +type wrappedCommandTemplate struct { + Command string +} + func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) { + wrappedCommand := func(command string) (string, error) { + ctx := b.config.ctx + ctx.Data = &wrappedCommandTemplate{Command: command} + return interpolate.Render(b.config.ChrootCommandWrapper, &ctx) + } + state := &multistep.BasicStateBag{} state.Put("config", &b.config) state.Put("client", b.client) state.Put("hook", hook) state.Put("ui", ui) + state.Put("wrappedCommand", CommandWrapper(wrappedCommand)) steps := []multistep.Step{ &stepCreateSSHKey{}, @@ -56,9 +68,28 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe Host: getPublicIP, SSHConfig: b.config.Comm.SSHConfigFunc(), }, - &common.StepProvision{}, - &stepStopVM{}, - &stepCreateImage{}, + } + + if b.config.ChrootDisk { + steps = append(steps, + &stepPrepareDevice{}, + &stepPreMountCommands{}, + &stepMountChroot{}, + &stepPostMountCommands{}, + &stepMountExtra{}, + &stepCopyFiles{}, + &stepChrootProvision{}, + &stepStopVM{}, + &stepDetachDisk{}, + &stepCreateVMFromDisk{}, + &stepCreateImage{}, + ) + } else { + steps = append(steps, + &common.StepProvision{}, + &stepStopVM{}, + &stepCreateImage{}, + ) } b.runner = common.NewRunner(steps, b.config.PackerConfig, ui) diff --git a/builder/hyperone/chroot_communicator.go b/builder/hyperone/chroot_communicator.go new file mode 100644 index 000000000..d0d9404f8 --- /dev/null +++ b/builder/hyperone/chroot_communicator.go @@ -0,0 +1,54 @@ +package hyperone + +import ( + "fmt" + "io" + "os" + "path/filepath" + "strconv" + + "github.com/hashicorp/packer/packer" +) + +type CommandWrapper func(string) (string, error) + +// ChrootCommunicator works as a wrapper on SSHCommunicator, modyfing paths in +// flight to be run in a chroot. +type ChrootCommunicator struct { + Chroot string + CmdWrapper CommandWrapper + Wrapped packer.Communicator +} + +func (c *ChrootCommunicator) Start(cmd *packer.RemoteCmd) error { + command := strconv.Quote(cmd.Command) + chrootCommand, err := c.CmdWrapper( + fmt.Sprintf("sudo chroot %s /bin/sh -c %s", c.Chroot, command)) + if err != nil { + return err + } + + cmd.Command = chrootCommand + + return c.Wrapped.Start(cmd) +} + +func (c *ChrootCommunicator) Upload(dst string, r io.Reader, fi *os.FileInfo) error { + dst = filepath.Join(c.Chroot, dst) + return c.Wrapped.Upload(dst, r, fi) +} + +func (c *ChrootCommunicator) UploadDir(dst string, src string, exclude []string) error { + dst = filepath.Join(c.Chroot, dst) + return c.Wrapped.UploadDir(dst, src, exclude) +} + +func (c *ChrootCommunicator) Download(src string, w io.Writer) error { + src = filepath.Join(c.Chroot, src) + return c.Wrapped.Download(src, w) +} + +func (c *ChrootCommunicator) DownloadDir(src string, dst string, exclude []string) error { + src = filepath.Join(c.Chroot, src) + return c.Wrapped.DownloadDir(src, dst, exclude) +} diff --git a/builder/hyperone/config.go b/builder/hyperone/config.go index 2a943e490..ca1cf9b2c 100644 --- a/builder/hyperone/config.go +++ b/builder/hyperone/config.go @@ -57,6 +57,19 @@ type Config struct { PrivateIP string `mapstructure:"private_ip"` PublicIP string `mapstructure:"public_ip"` + ChrootDisk bool `mapstructure:"chroot_disk"` + ChrootDiskSize float32 `mapstructure:"chroot_disk_size"` + ChrootDiskType string `mapstructure:"chroot_disk_type"` + ChrootMountPath string `mapstructure:"chroot_mount_path"` + ChrootMounts [][]string `mapstructure:"chroot_mounts"` + ChrootCopyFiles []string `mapstructure:"chroot_copy_files"` + ChrootCommandWrapper string `mapstructure:"chroot_command_wrapper"` + + MountOptions []string `mapstructure:"mount_options"` + MountPartition string `mapstructure:"mount_partition"` + PreMountCommands []string `mapstructure:"pre_mount_commands"` + PostMountCommands []string `mapstructure:"post_mount_commands"` + SSHKeys []string `mapstructure:"ssh_keys"` UserData string `mapstructure:"user_data"` @@ -74,6 +87,10 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) { InterpolateFilter: &interpolate.RenderFilter{ Exclude: []string{ "run_command", + "chroot_command_wrapper", + "post_mount_commands", + "pre_mount_commands", + "mount_path", }, }, }, raws...) @@ -138,6 +155,45 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) { c.DiskType = defaultDiskType } + if c.ChrootCommandWrapper == "" { + c.ChrootCommandWrapper = "{{.Command}}" + } + + if c.ChrootDiskSize == 0 { + c.ChrootDiskSize = c.DiskSize + } + + if c.ChrootDiskType == "" { + c.ChrootDiskType = c.DiskType + } + + if c.ChrootMountPath == "" { + c.ChrootMountPath = "/mnt/packer-hyperone-volumes/{{timestamp}}" + } + + if c.ChrootMounts == nil { + c.ChrootMounts = make([][]string, 0) + } + + if len(c.ChrootMounts) == 0 { + c.ChrootMounts = [][]string{ + {"proc", "proc", "/proc"}, + {"sysfs", "sysfs", "/sys"}, + {"bind", "/dev", "/dev"}, + {"devpts", "devpts", "/dev/pts"}, + {"binfmt_misc", "binfmt_misc", "/proc/sys/fs/binfmt_misc"}, + } + } + + if c.ChrootCopyFiles == nil { + c.ChrootCopyFiles = []string{"/etc/resolv.conf"} + } + + if c.MountPartition == "" { + c.MountPartition = "1" + } + + // Validation var errs *packer.MultiError if es := c.Comm.Prepare(&c.ctx); len(es) > 0 { errs = packer.MultiErrorAppend(errs, es...) @@ -159,6 +215,20 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) { errs = packer.MultiErrorAppend(errs, errors.New("source image is required")) } + if c.ChrootDisk { + if len(c.PreMountCommands) == 0 { + errs = packer.MultiErrorAppend(errs, errors.New("pre-mount commands are required for chroot disk")) + } + } + + for _, mounts := range c.ChrootMounts { + if len(mounts) != 3 { + errs = packer.MultiErrorAppend( + errs, errors.New("each chroot_mounts entry should have three elements")) + break + } + } + if errs != nil && len(errs.Errors) > 0 { return nil, nil, errs } diff --git a/builder/hyperone/step_chroot_provision.go b/builder/hyperone/step_chroot_provision.go new file mode 100644 index 000000000..005c869c5 --- /dev/null +++ b/builder/hyperone/step_chroot_provision.go @@ -0,0 +1,28 @@ +package hyperone + +import ( + "context" + + "github.com/hashicorp/packer/common" + "github.com/hashicorp/packer/helper/multistep" + "github.com/hashicorp/packer/packer" +) + +type stepChrootProvision struct{} + +func (s *stepChrootProvision) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { + config := state.Get("config").(*Config) + wrappedCommand := state.Get("wrappedCommand").(CommandWrapper) + sshCommunicator := state.Get("communicator").(packer.Communicator) + + comm := &ChrootCommunicator{ + Chroot: config.ChrootMountPath, + CmdWrapper: wrappedCommand, + Wrapped: sshCommunicator, + } + + stepProvision := common.StepProvision{Comm: comm} + return stepProvision.Run(ctx, state) +} + +func (s *stepChrootProvision) Cleanup(multistep.StateBag) {} diff --git a/builder/hyperone/step_copy_files.go b/builder/hyperone/step_copy_files.go new file mode 100644 index 000000000..685f4ab5d --- /dev/null +++ b/builder/hyperone/step_copy_files.go @@ -0,0 +1,40 @@ +package hyperone + +import ( + "context" + "fmt" + "log" + "path/filepath" + + "github.com/hashicorp/packer/helper/multistep" + "github.com/hashicorp/packer/packer" +) + +type stepCopyFiles struct{} + +func (s *stepCopyFiles) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { + config := state.Get("config").(*Config) + ui := state.Get("ui").(packer.Ui) + + if len(config.ChrootCopyFiles) == 0 { + return multistep.ActionContinue + } + + ui.Say("Copying files from host to chroot...") + for _, path := range config.ChrootCopyFiles { + chrootPath := filepath.Join(config.ChrootMountPath, path) + log.Printf("Copying '%s' to '%s'", path, chrootPath) + + command := fmt.Sprintf("cp --remove-destination %s %s", path, chrootPath) + err := runCommands([]string{command}, config.ctx, state) + if err != nil { + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + } + + return multistep.ActionContinue +} + +func (s *stepCopyFiles) Cleanup(state multistep.StateBag) {} diff --git a/builder/hyperone/step_create_vm.go b/builder/hyperone/step_create_vm.go index eafcf8a98..7da448be9 100644 --- a/builder/hyperone/step_create_vm.go +++ b/builder/hyperone/step_create_vm.go @@ -13,6 +13,10 @@ type stepCreateVM struct { vmID string } +const ( + chrootDiskName = "packer-chroot-disk" +) + func (s *stepCreateVM) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { client := state.Get("client").(*openapi.APIClient) ui := state.Get("ui").(packer.Ui) @@ -26,17 +30,27 @@ func (s *stepCreateVM) Run(ctx context.Context, state multistep.StateBag) multis var sshKeys = []string{sshKey} sshKeys = append(sshKeys, config.SSHKeys...) - options := openapi.VmCreate{ - Name: config.VmName, - Image: config.SourceImage, - Service: config.VmFlavour, - SshKeys: sshKeys, - Disk: []openapi.VmCreateDisk{ - { - Service: config.DiskType, - Size: config.DiskSize, - }, + disks := []openapi.VmCreateDisk{ + { + Service: config.DiskType, + Size: config.DiskSize, }, + } + + if config.ChrootDisk { + disks = append(disks, openapi.VmCreateDisk{ + Service: config.ChrootDiskType, + Size: config.ChrootDiskSize, + Name: chrootDiskName, + }) + } + + options := openapi.VmCreate{ + Name: config.VmName, + Image: config.SourceImage, + Service: config.VmFlavour, + SshKeys: sshKeys, + Disk: disks, Netadp: []openapi.VmCreateNetadp{netAdapter}, UserMetadata: config.UserData, Tag: config.VmTags, @@ -63,7 +77,11 @@ func (s *stepCreateVM) Run(ctx context.Context, state multistep.StateBag) multis var diskIDs []string for _, hdd := range hdds { - diskIDs = append(diskIDs, hdd.Disk.Id) + if hdd.Disk.Name == chrootDiskName { + state.Put("chroot_disk_id", hdd.Disk.Id) + } else { + diskIDs = append(diskIDs, hdd.Disk.Id) + } } state.Put("disk_ids", diskIDs) diff --git a/builder/hyperone/step_create_vm_from_disk.go b/builder/hyperone/step_create_vm_from_disk.go new file mode 100644 index 000000000..9542c2849 --- /dev/null +++ b/builder/hyperone/step_create_vm_from_disk.go @@ -0,0 +1,70 @@ +package hyperone + +import ( + "context" + "fmt" + + "github.com/hashicorp/packer/helper/multistep" + "github.com/hashicorp/packer/packer" + "github.com/hyperonecom/h1-client-go" +) + +type stepCreateVMFromDisk struct { + vmID string +} + +func (s *stepCreateVMFromDisk) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { + client := state.Get("client").(*openapi.APIClient) + ui := state.Get("ui").(packer.Ui) + config := state.Get("config").(*Config) + sshKey := state.Get("ssh_public_key").(string) + chrootDiskID := state.Get("chroot_disk_id").(string) + + ui.Say("Creating VM from disk...") + + options := openapi.VmCreate{ + Name: config.VmName, + Service: config.VmFlavour, + Disk: []openapi.VmCreateDisk{ + { + Id: chrootDiskID, + }, + }, + SshKeys: []string{sshKey}, + Boot: false, + } + + vm, _, err := client.VmApi.VmCreate(ctx, options) + if err != nil { + err := fmt.Errorf("error creating VM from disk: %s", formatOpenAPIError(err)) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + s.vmID = vm.Id + state.Put("vm_id", vm.Id) + + return multistep.ActionContinue +} + +func (s *stepCreateVMFromDisk) Cleanup(state multistep.StateBag) { + if s.vmID == "" { + return + } + + client := state.Get("client").(*openapi.APIClient) + ui := state.Get("ui").(packer.Ui) + chrootDiskID := state.Get("chroot_disk_id").(string) + + ui.Say("Deleting VM (from disk)...") + + deleteOptions := openapi.VmDelete{ + RemoveDisks: []string{chrootDiskID}, + } + + _, err := client.VmApi.VmDelete(context.TODO(), s.vmID, deleteOptions) + if err != nil { + ui.Error(fmt.Sprintf("Error deleting server '%s' - please delete it manually: %s", s.vmID, formatOpenAPIError(err))) + } +} diff --git a/builder/hyperone/step_detach_disk.go b/builder/hyperone/step_detach_disk.go new file mode 100644 index 000000000..3f96e8acc --- /dev/null +++ b/builder/hyperone/step_detach_disk.go @@ -0,0 +1,34 @@ +package hyperone + +import ( + "context" + "fmt" + + "github.com/hashicorp/packer/helper/multistep" + "github.com/hashicorp/packer/packer" + "github.com/hyperonecom/h1-client-go" +) + +type stepDetachDisk struct { + vmID string +} + +func (s *stepDetachDisk) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { + client := state.Get("client").(*openapi.APIClient) + ui := state.Get("ui").(packer.Ui) + vmID := state.Get("vm_id").(string) + chrootDiskID := state.Get("chroot_disk_id").(string) + + ui.Say("Detaching chroot disk...") + _, _, err := client.VmApi.VmDeleteHddDiskId(ctx, vmID, chrootDiskID) + if err != nil { + err := fmt.Errorf("error detaching disk: %s", formatOpenAPIError(err)) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + return multistep.ActionContinue +} + +func (s *stepDetachDisk) Cleanup(state multistep.StateBag) {} diff --git a/builder/hyperone/step_mount_chroot.go b/builder/hyperone/step_mount_chroot.go new file mode 100644 index 000000000..a4e01a09c --- /dev/null +++ b/builder/hyperone/step_mount_chroot.go @@ -0,0 +1,51 @@ +package hyperone + +import ( + "context" + "fmt" + "log" + "strings" + + "github.com/hashicorp/packer/helper/multistep" + "github.com/hashicorp/packer/packer" +) + +type stepMountChroot struct{} + +func (s *stepMountChroot) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { + config := state.Get("config").(*Config) + ui := state.Get("ui").(packer.Ui) + device := state.Get("device").(string) + + log.Printf("Mount path: %s", config.ChrootMountPath) + + ui.Say(fmt.Sprintf("Creating mount directory: %s", config.ChrootMountPath)) + + opts := "" + if len(config.MountOptions) > 0 { + opts = "-o " + strings.Join(config.MountOptions, " -o ") + } + + deviceMount := device + if config.MountPartition != "" { + deviceMount = fmt.Sprintf("%s%s", device, config.MountPartition) + } + + commands := []string{ + fmt.Sprintf("mkdir -m 755 -p %s", config.ChrootMountPath), + fmt.Sprintf("mount %s %s %s", opts, deviceMount, config.ChrootMountPath), + } + + err := runCommands(commands, config.ctx, state) + if err != nil { + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + state.Put("mount_path", config.ChrootMountPath) + + return multistep.ActionContinue +} + +func (s *stepMountChroot) Cleanup(state multistep.StateBag) {} diff --git a/builder/hyperone/step_mount_extra.go b/builder/hyperone/step_mount_extra.go new file mode 100644 index 000000000..0449b6a3f --- /dev/null +++ b/builder/hyperone/step_mount_extra.go @@ -0,0 +1,45 @@ +package hyperone + +import ( + "context" + "fmt" + + "github.com/hashicorp/packer/helper/multistep" + "github.com/hashicorp/packer/packer" +) + +type stepMountExtra struct{} + +func (s *stepMountExtra) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { + config := state.Get("config").(*Config) + mountPath := state.Get("mount_path").(string) + ui := state.Get("ui").(packer.Ui) + + ui.Say("Mounting additional paths within the chroot...") + for _, mountInfo := range config.ChrootMounts { + innerPath := mountPath + mountInfo[2] + + flags := "-t " + mountInfo[0] + if mountInfo[0] == "bind" { + flags = "--bind" + } + + ui.Message(fmt.Sprintf("Mounting: %s", mountInfo[2])) + + commands := []string{ + fmt.Sprintf("mkdir -m 755 -p %s", innerPath), + fmt.Sprintf("mount %s %s %s", flags, mountInfo[1], innerPath), + } + + err := runCommands(commands, config.ctx, state) + if err != nil { + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + } + + return multistep.ActionContinue +} + +func (s *stepMountExtra) Cleanup(state multistep.StateBag) {} diff --git a/builder/hyperone/step_post_mount_commands.go b/builder/hyperone/step_post_mount_commands.go new file mode 100644 index 000000000..e0f3ff502 --- /dev/null +++ b/builder/hyperone/step_post_mount_commands.go @@ -0,0 +1,37 @@ +package hyperone + +import ( + "context" + + "github.com/hashicorp/packer/helper/multistep" + "github.com/hashicorp/packer/packer" +) + +type postMountCommandsData struct { + Device string + MountPath string +} + +type stepPostMountCommands struct{} + +func (s *stepPostMountCommands) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { + config := state.Get("config").(*Config) + ui := state.Get("ui").(packer.Ui) + device := state.Get("device").(string) + + ctx := config.ctx + ctx.Data = &postMountCommandsData{ + Device: device, + MountPath: config.ChrootMountPath, + } + + ui.Say("Running post-mount commands...") + if err := runCommands(config.PostMountCommands, ctx, state); err != nil { + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + return multistep.ActionContinue +} + +func (s *stepPostMountCommands) Cleanup(state multistep.StateBag) {} diff --git a/builder/hyperone/step_pre_mount_commands.go b/builder/hyperone/step_pre_mount_commands.go new file mode 100644 index 000000000..459bff6c3 --- /dev/null +++ b/builder/hyperone/step_pre_mount_commands.go @@ -0,0 +1,37 @@ +package hyperone + +import ( + "context" + + "github.com/hashicorp/packer/helper/multistep" + "github.com/hashicorp/packer/packer" +) + +type preMountCommandsData struct { + Device string + MountPath string +} + +type stepPreMountCommands struct{} + +func (s *stepPreMountCommands) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { + config := state.Get("config").(*Config) + ui := state.Get("ui").(packer.Ui) + device := state.Get("device").(string) + + ctx := config.ctx + ctx.Data = &preMountCommandsData{ + Device: device, + MountPath: config.ChrootMountPath, + } + + ui.Say("Running pre-mount commands...") + if err := runCommands(config.PreMountCommands, ctx, state); err != nil { + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + return multistep.ActionContinue +} + +func (s *stepPreMountCommands) Cleanup(state multistep.StateBag) {} diff --git a/builder/hyperone/step_prepare_device.go b/builder/hyperone/step_prepare_device.go new file mode 100644 index 000000000..b282f1e4b --- /dev/null +++ b/builder/hyperone/step_prepare_device.go @@ -0,0 +1,46 @@ +package hyperone + +import ( + "context" + "fmt" + "log" + "os" + + "github.com/hashicorp/packer/helper/multistep" + "github.com/hashicorp/packer/packer" +) + +type stepPrepareDevice struct{} + +func (s *stepPrepareDevice) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { + ui := state.Get("ui").(packer.Ui) + chrootDiskID := state.Get("chroot_disk_id").(string) + + var err error + log.Println("Searching for available device...") + device, err := availableDevice(chrootDiskID) + if err != nil { + err := fmt.Errorf("error finding available device: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + if _, err := os.Stat(device); err == nil { + err := fmt.Errorf("device is in use: %s", device) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + ui.Say(fmt.Sprintf("Found device: %s", device)) + state.Put("device", device) + return multistep.ActionContinue +} + +func (s *stepPrepareDevice) Cleanup(state multistep.StateBag) {} + +func availableDevice(scsiID string) (string, error) { + // TODO proper SCSI search + return "/dev/sdb", nil +} diff --git a/builder/hyperone/utils.go b/builder/hyperone/utils.go index 3105693de..27d172c68 100644 --- a/builder/hyperone/utils.go +++ b/builder/hyperone/utils.go @@ -3,6 +3,9 @@ package hyperone import ( "fmt" + "github.com/hashicorp/packer/helper/multistep" + "github.com/hashicorp/packer/packer" + "github.com/hashicorp/packer/template/interpolate" "github.com/hyperonecom/h1-client-go" ) @@ -14,3 +17,40 @@ func formatOpenAPIError(err error) string { return fmt.Sprintf("%s (body: %s)", openAPIError.Error(), openAPIError.Body()) } + +func runCommands(commands []string, ctx interpolate.Context, state multistep.StateBag) error { + ui := state.Get("ui").(packer.Ui) + wrappedCommand := state.Get("wrappedCommand").(CommandWrapper) + comm := state.Get("communicator").(packer.Communicator) + + for _, rawCmd := range commands { + intCmd, err := interpolate.Render(rawCmd, &ctx) + if err != nil { + return fmt.Errorf("error interpolating: %s", err) + } + + command, err := wrappedCommand(intCmd) + if err != nil { + return fmt.Errorf("error wrapping command: %s", err) + } + + remoteCmd := &packer.RemoteCmd{ + Command: command, + } + + ui.Say(fmt.Sprintf("Executing command: %s", command)) + + err = remoteCmd.StartWithUi(comm, ui) + if err != nil { + return fmt.Errorf("error running remote cmd: %s", err) + } + + if remoteCmd.ExitStatus != 0 { + return fmt.Errorf( + "received non-zero exit code %d from command: %s", + remoteCmd.ExitStatus, + command) + } + } + return nil +} diff --git a/examples/hyperone/chroot.json b/examples/hyperone/chroot.json new file mode 100644 index 000000000..94a0dbdb5 --- /dev/null +++ b/examples/hyperone/chroot.json @@ -0,0 +1,29 @@ +{ + "variables": { + "token": "{{ env `HYPERONE_TOKEN` }}", + "project": "{{ env `HYPERONE_PROJECT` }}" + }, + "builders": [ + { + "token": "{{ user `token` }}", + "project": "{{ user `project` }}", + "type": "hyperone", + "source_image": "ubuntu", + "disk_size": 10, + "vm_type": "a1.nano", + + "chroot_disk": true, + "chroot_command_wrapper": "sudo {{.Command}}", + "pre_mount_commands": [ + "parted {{.Device}} mklabel msdos mkpart primary 1M 100% set 1 boot on print", + "mkfs.ext4 {{.Device}}1" + ], + "post_mount_commands": [ + "apt-get update", + "apt-get install debootstrap", + "debootstrap --arch amd64 bionic {{.MountPath}}" + ] + } + ], + "provisioners": [] +} diff --git a/website/source/docs/builders/hyperone.html.md b/website/source/docs/builders/hyperone.html.md index dfa61d021..da7ac4040 100644 --- a/website/source/docs/builders/hyperone.html.md +++ b/website/source/docs/builders/hyperone.html.md @@ -114,7 +114,7 @@ builder. - `disk_name` (string) - The name of the created disk. -- `disk_type` (string) - The type of the created disk. +- `disk_type` (string) - The type of the created disk. Defaults to `ssd`. - `image_description` (string) - The description of the resulting image. @@ -154,6 +154,57 @@ builder. - `vm_tags` (map of key/value strings) - Key/value pair tags to add to the created server. +## Chroot disk + +### Required: + +- `chroot_disk` (bool) - Set to `true` to enable chroot disk build. + +- `pre_mount_commands` (array of strings) - A series of commands to execute + before mounting the chroot. This should include any partitioning and + filesystem creation commands. The path to the device is provided by + `{{.Device}}`. + +### Optional: + +- `chroot_command_wrapper` (string) - How to run shell commands. This defaults + to `{{.Command}}`. This may be useful to set if you want to set + environment variables or run commands with `sudo`. + +- `chroot_copy_files` (array of strings) - Paths to files on the running VM + that will be copied into the chroot environment before provisioning. + Defaults to `/etc/resolv.conf` so that DNS lookups work. + +- `chroot_disk_size` (float) - The size of the chroot disk in GiB. Defaults + to `disk_size`. + +- `chroot_disk_type` (string) - The type of the chroot disk. Defaults to + `disk_type`. + +- `chroot_mount_path` (string) - The path on which the device will be mounted. + +- `chroot_mounts` (array of strings) - A list of devices to mount into the + chroot environment. This is a list of 3-element tuples, in order: + + - The filesystem type. If this is "bind", then Packer will properly bind the + filesystem to another mount point. + + - The source device. + + - The mount directory. + +- `mount_options` (array of tuples) - Options to supply the `mount` command + when mounting devices. Each option will be prefixed with `-o` and supplied + to the `mount` command. + +- `mount_partition` (string) - The partition number containing the / partition. + By default this is the first partition of the volume (for example, sdb1). + +- `post_mount_commands` (array of strings) - As `pre_mount_commands`, but the + commands are executed after mounting the root device and before the extra + mount and copy steps. The device and mount path are provided by + `{{.Device}}` and `{{.MountPath}}`. + ## Basic Example Here is a basic example. It is completely valid as soon as you enter your own @@ -161,10 +212,29 @@ token. ``` json { - "type": "hyperone", - "token": "YOUR_AUTH_TOKEN", - "source_image": "ubuntu-18.04", - "vm_flavour": "a1.nano", - "disk_size": 10 + "type": "hyperone", + "token": "YOUR_AUTH_TOKEN", + "source_image": "ubuntu-18.04", + "vm_flavour": "a1.nano", + "disk_size": 10 +} +``` + +## Chroot Example + + +``` json +{ + "type": "hyperone", + "token": "YOUR_AUTH_TOKEN", + "source_image": "ubuntu-18.04", + "vm_flavour": "a1.nano", + "disk_size": 10, + "chroot_disk": true, + "pre_mount_commands": [ + "apt-get update", + "apt-get install debootstrap", + "debootstrap --arch amd64 bionic {{.MountPath}}" + ] } ```