diff --git a/README.md b/README.md index 7ff2a73..2d89c99 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ This plugin allows you to build or extend ARM system image. It operates in two m * new - creates empty disk image and populates the rootfs on it * reuse - uses already existing image as the base * resize - uses already existing image but resize given partition (ie. root) +* repartition - uses already existing image but recreates partition table and optionally formats selected partitions Plugin mimics standard image creation process, such as: * building base empty image (dd) @@ -226,6 +227,59 @@ Complete examples: - [`boards/raspberry-pi/raspbian-resize.json`](./boards/raspberry-pi/raspbian-resize.json) - [`boards/beaglebone-black/ubuntu.pkr.hcl`](./boards/beaglebone-black/ubuntu.pkr.hcl) +## Repartitioning image + +The `repartition` method allows for rewriting the partition table of an image, formatting any partitions (opt-out with `skip_mkfs`), and resizing the filesystems on the partition to the size of the partition with `resize2fs`. This is similar in outcome to the `new` method, however may be used with raw image files (`.img` or `.iso`), rather than rootfs archives. + +To repartition an image: + +* Set `image_build_method` to `repartition` +* Ensure all partitions are fully configured in `image_partitions` + * To **prevent** a partition from being reformatted, set `skip_mkfs` to `true` (`false` by default) + * To invoke resize2fs on a partition after the partition table has been rewritten (this is necessary if you change the partition size), set `resize_fs` to `true` (`false` by default). **Note**: as resize2fs only operates on ext{2,3,4} partitions, non-ext partitions cannot be resized. + +For example: + +```json +"builders": [ + { + "type": "arm", + "image_build_method": "resize", + "image_partitions": [ + { + "name": "boot", + "start_sector": "2048", + "size": "524288", // 256MiB + "skip_mkfs": true, + ... + }, + { + "name": "root", + "size": "16777216", // 8GiB + "start_sector": "526336", + "skip_mkfs": true, + "resize_fs": true, + ... + }, + { + "name": "home", + "size": "0", // expand to end of disk + "start_sector": "17303552", + "resize_fs": true, + ... + } + ], + ... + } +] +``` + +Assuming a source image with a boot partition of 256MiB and a root partition of 4GiB, the above configuration will: + +* Keep the boot partition as-is +* Resize the root partition to 8GiB +* Create and format a new partition which fills the remainder of the disk + ## Docker With `artifice` plugin you can pass rootfs archive to docker plugins ``` diff --git a/builder/builder.go b/builder/builder.go index 376a379..318275a 100644 --- a/builder/builder.go +++ b/builder/builder.go @@ -149,6 +149,18 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack &StepMountImage{FromKey: "image_loop_device", ResultKey: "image_mountpoint", MountPath: b.config.ImageMountPath}, ) + case "repartition": + steps = append( + steps, + &StepExtractAndCopyImage{FromKey: "rootfs_archive_path"}, + &StepResizeQemuImage{}, + &StepPartitionImage{}, + &StepMapImage{ResultKey: "image_loop_device"}, + &StepMkfsImage{FromKey: "image_loop_device"}, + &StepResizePartitionFs{FromKey: "image_loop_device"}, + &StepMountImage{FromKey: "image_loop_device", ResultKey: "image_mountpoint", MountPath: b.config.ImageMountPath}, + ) + default: return nil, errors.New("invalid build method") } diff --git a/builder/step_mkfs_image.go b/builder/step_mkfs_image.go index 2ef617d..13959c3 100644 --- a/builder/step_mkfs_image.go +++ b/builder/step_mkfs_image.go @@ -22,6 +22,11 @@ func (s *StepMkfsImage) Run(_ context.Context, state multistep.StateBag) multist loopDevice := state.Get(s.FromKey).(string) for i, partition := range config.ImageConfig.ImagePartitions { + if partition.SkipMkfs { + ui.Message(fmt.Sprintf("skipping mkfs for partition #%d", i+1)) + continue + } + cmd := fmt.Sprintf("mkfs.%s", partition.Filesystem) args := append(partition.FilesystemMakeOptions, fmt.Sprintf("%sp%d", loopDevice, i+1)) diff --git a/builder/step_resize_partition_fs.go b/builder/step_resize_partition_fs.go index faff859..bbd8054 100644 --- a/builder/step_resize_partition_fs.go +++ b/builder/step_resize_partition_fs.go @@ -7,6 +7,7 @@ import ( "github.com/hashicorp/packer-plugin-sdk/multistep" "github.com/hashicorp/packer-plugin-sdk/packer" + "github.com/mkaczanowski/packer-builder-arm/config" ) // StepResizePartitionFs expand already partitioned image @@ -15,21 +16,48 @@ type StepResizePartitionFs struct { SelectedPartitionKey string } +// Find partitions marked with ResizeFs that we explicitly want to resize +func findPartitionsToResize(imagePartitions []config.Partition) []int { + var selectedPartitions []int + + for i, partition := range imagePartitions { + if partition.ResizeFs { + selectedPartitions = append(selectedPartitions, i+1) + } + } + + return selectedPartitions +} + // Run the step func (s *StepResizePartitionFs) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { var ( - ui = state.Get("ui").(packer.Ui) - - loopDevice = state.Get(s.FromKey).(string) - selectedPartition = state.Get(s.SelectedPartitionKey).(int) - device = fmt.Sprintf("%sp%d", loopDevice, selectedPartition) + ui = state.Get("ui").(packer.Ui) + loopDevice = state.Get(s.FromKey).(string) ) - out, err := exec.Command("resize2fs", "-f", device).CombinedOutput() - ui.Message(fmt.Sprintf("running resize2fs on %s ", device)) - if err != nil { - ui.Error(fmt.Sprintf("error while resizing partition %v: %s", err, out)) - return multistep.ActionHalt + var selectedPartitions []int + + // If we're running in resize mode, we'll have a single selected partition + // to resize from StepExpandPartition, and we can be sure we don't need to + // expand any other partitions. If we're in repartition mode, we manually + // choose which partitions to resize: only the user knows which ones + // have been expanded. + if s.SelectedPartitionKey != "" { + selectedPartitions = append(selectedPartitions, state.Get(s.SelectedPartitionKey).(int)) + } else { + config := state.Get("config").(*Config) + selectedPartitions = findPartitionsToResize(config.ImagePartitions) + } + + for _, partition := range selectedPartitions { + device := fmt.Sprintf("%sp%d", loopDevice, partition) + out, err := exec.Command("resize2fs", "-f", device).CombinedOutput() + ui.Message(fmt.Sprintf("running resize2fs on %s ", device)) + if err != nil { + ui.Error(fmt.Sprintf("error while resizing partition %v: %s", err, out)) + return multistep.ActionHalt + } } return multistep.ActionContinue diff --git a/config/image_config.go b/config/image_config.go index 3ab0e04..849ff8d 100644 --- a/config/image_config.go +++ b/config/image_config.go @@ -19,6 +19,8 @@ type Partition struct { Filesystem string `mapstructure:"filesystem"` FilesystemMakeOptions []string `mapstructure:"filesystem_make_options"` Mountpoint string `mapstructure:"mountpoint"` + ResizeFs bool `mapstructure:"resize_fs"` + SkipMkfs bool `mapstructure:"skip_mkfs"` } // ChrootMount describes a mountpoint that is being setup @@ -74,8 +76,8 @@ func (c *ImageConfig) Prepare(_ *interpolate.Context) (warnings []string, errs [ errs = append(errs, errors.New("image build method must be specified")) } - if !(c.ImageBuildMethod == "new" || c.ImageBuildMethod == "reuse" || c.ImageBuildMethod == "resize") { - errs = append(errs, errors.New("invalid image build method specified (valid options: new, reuse)")) + if !(c.ImageBuildMethod == "new" || c.ImageBuildMethod == "reuse" || c.ImageBuildMethod == "resize" || c.ImageBuildMethod == "repartition") { + errs = append(errs, errors.New("invalid image build method specified (valid options: new, reuse, resize, repartition)")) } if len(c.ImagePartitions) == 0 { diff --git a/config/image_config.hcl2spec.go b/config/image_config.hcl2spec.go index f44ca7d..c9493e0 100644 --- a/config/image_config.hcl2spec.go +++ b/config/image_config.hcl2spec.go @@ -45,6 +45,8 @@ type FlatPartition struct { Filesystem *string `mapstructure:"filesystem" cty:"filesystem" hcl:"filesystem"` FilesystemMakeOptions []string `mapstructure:"filesystem_make_options" cty:"filesystem_make_options" hcl:"filesystem_make_options"` Mountpoint *string `mapstructure:"mountpoint" cty:"mountpoint" hcl:"mountpoint"` + ResizeFs *bool `mapstructure:"resize_fs" cty:"resize_fs" hcl:"resize_fs"` + SkipMkfs *bool `mapstructure:"skip_mkfs" cty:"skip_mkfs" hcl:"skip_mkfs"` } // FlatMapstructure returns a new FlatPartition. @@ -67,6 +69,8 @@ func (*FlatPartition) HCL2Spec() map[string]hcldec.Spec { "filesystem": &hcldec.AttrSpec{Name: "filesystem", Type: cty.String, Required: false}, "filesystem_make_options": &hcldec.AttrSpec{Name: "filesystem_make_options", Type: cty.List(cty.String), Required: false}, "mountpoint": &hcldec.AttrSpec{Name: "mountpoint", Type: cty.String, Required: false}, + "resize_fs": &hcldec.AttrSpec{Name: "resize_fs", Type: cty.Bool, Required: false}, + "skip_mkfs": &hcldec.AttrSpec{Name: "skip_mkfs", Type: cty.Bool, Required: false}, } return s }