diff options
Diffstat (limited to 'actions/image_partition_action.go')
-rw-r--r-- | actions/image_partition_action.go | 432 |
1 files changed, 432 insertions, 0 deletions
diff --git a/actions/image_partition_action.go b/actions/image_partition_action.go new file mode 100644 index 0000000..5054e6d --- /dev/null +++ b/actions/image_partition_action.go @@ -0,0 +1,432 @@ +/* +ImagePartition Action + +This action creates an image file, partitions it and formats the filesystems. + +Yaml syntax: + - action: image-partition + imagename: image_name + imagesize: size + partitiontype: gpt + gpt_gap: offset + partitions: + <list of partitions> + mountpoints: + <list of mount points> + +Mandatory properties: + +- imagename -- the name of the image file. + +- imagesize -- generated image size in human-readable form, examples: 100MB, 1GB, etc. + +- partitiontype -- partition table type. Currently only 'gpt' and 'msdos' +partition tables are supported. + +- gpt_gap -- shifting GPT allow to use this gap for bootloaders, for example if +U-Boot intersects with original GPT placement. +Only works if parted supports an extra argument to mklabel to specify the gpt offset. + +- partitions -- list of partitions, at least one partition is needed. +Partition properties are described below. + +- mountpoints -- list of mount points for partitions. +Properties for mount points are described below. + +Yaml syntax for partitions: + + partitions: + - name: label + name: partition name + fs: filesystem + start: offset + end: offset + flags: list of flags + +Mandatory properties: + +- name -- is used for referencing named partition for mount points +configuration (below) and label the filesystem located on this partition. + +- fs -- filesystem type used for formatting. + +'none' fs type should be used for partition without filesystem. + +- start -- offset from beginning of the disk there the partition starts. + +- end -- offset from beginning of the disk there the partition ends. + +For 'start' and 'end' properties offset can be written in human readable +form -- '32MB', '1GB' or as disk percentage -- '100%'. + +Optional properties: + +- flags -- list of additional flags for partition compatible with parted(8) +'set' command. + +Yaml syntax for mount points: + + mountpoints: + - mountpoint: path + partition: partition label + options: list of options + +Mandatory properties: + +- partition -- partition name for mounting. + +- mountpoint -- path in the target root filesystem where the named partition +should be mounted. + +Optional properties: + +- options -- list of options to be added to appropriate entry in fstab file. + +Layout example for Raspberry PI 3: + + - action: image-partition + imagename: "debian-rpi3.img" + imagesize: 1GB + partitiontype: msdos + mountpoints: + - mountpoint: / + partition: root + - mountpoint: /boot/firmware + partition: firmware + options: [ x-systemd.automount ] + partitions: + - name: firmware + fs: vfat + start: 0% + end: 64MB + - name: root + fs: ext4 + start: 64MB + end: 100% + flags: [ boot ] +*/ +package actions + +import ( + "errors" + "fmt" + "github.com/docker/go-units" + "github.com/go-debos/fakemachine" + "log" + "os" + "os/exec" + "path" + "strings" + "syscall" + + "github.com/go-debos/debos" +) + +type Partition struct { + number int + Name string + Start string + End string + FS string + Flags []string + FSUUID string +} + +type Mountpoint struct { + Mountpoint string + Partition string + Options []string + part *Partition +} + +type ImagePartitionAction struct { + debos.BaseAction `yaml:",inline"` + ImageName string + ImageSize string + PartitionType string + GptGap string "gpt_gap" + Partitions []Partition + Mountpoints []Mountpoint + size int64 + usingLoop bool +} + +func (i *ImagePartitionAction) generateFSTab(context *debos.DebosContext) error { + context.ImageFSTab.Reset() + + for _, m := range i.Mountpoints { + options := []string{"defaults"} + options = append(options, m.Options...) + if m.part.FSUUID == "" { + return fmt.Errorf("Missing fs UUID for partition %s!?!", m.part.Name) + } + context.ImageFSTab.WriteString(fmt.Sprintf("UUID=%s\t%s\t%s\t%s\t0\t0\n", + m.part.FSUUID, m.Mountpoint, m.part.FS, + strings.Join(options, ","))) + } + + return nil +} + +func (i *ImagePartitionAction) generateKernelRoot(context *debos.DebosContext) error { + for _, m := range i.Mountpoints { + if m.Mountpoint == "/" { + if m.part.FSUUID == "" { + return errors.New("No fs UUID for root partition !?!") + } + context.ImageKernelRoot = fmt.Sprintf("root=UUID=%s", m.part.FSUUID) + break + } + } + + return nil +} + +func (i ImagePartitionAction) getPartitionDevice(number int, context debos.DebosContext) string { + suffix := "p" + /* Check partition naming first: if used 'by-id'i naming convention */ + if strings.Contains(context.Image, "/disk/by-id/") { + suffix = "-part" + } + + /* If the iamge device has a digit as the last character, the partition + * suffix is p<number> else it's just <number> */ + last := context.Image[len(context.Image)-1] + if last >= '0' && last <= '9' { + return fmt.Sprintf("%s%s%d", context.Image, suffix, number) + } else { + return fmt.Sprintf("%s%d", context.Image, number) + } +} + +func (i ImagePartitionAction) PreMachine(context *debos.DebosContext, m *fakemachine.Machine, + args *[]string) error { + image, err := m.CreateImage(i.ImageName, i.size) + if err != nil { + return err + } + + context.Image = image + *args = append(*args, "--internal-image", image) + return nil +} + +func (i ImagePartitionAction) formatPartition(p *Partition, context debos.DebosContext) error { + label := fmt.Sprintf("Formatting partition %d", p.number) + path := i.getPartitionDevice(p.number, context) + + cmdline := []string{} + switch p.FS { + case "vfat": + cmdline = append(cmdline, "mkfs.vfat", "-n", p.Name) + case "btrfs": + // Force formatting to prevent failure in case if partition was formatted already + cmdline = append(cmdline, "mkfs.btrfs", "-L", p.Name, "-f") + case "none": + default: + cmdline = append(cmdline, fmt.Sprintf("mkfs.%s", p.FS), "-L", p.Name) + } + + if len(cmdline) != 0 { + cmdline = append(cmdline, path) + + cmd := debos.Command{} + if err := cmd.Run(label, cmdline...); err != nil { + return err + } + } + + if p.FS != "none" { + uuid, err := exec.Command("blkid", "-o", "value", "-s", "UUID", "-p", "-c", "none", path).Output() + if err != nil { + return fmt.Errorf("Failed to get uuid: %s", err) + } + p.FSUUID = strings.TrimSpace(string(uuid[:])) + } + + return nil +} + +func (i ImagePartitionAction) PreNoMachine(context *debos.DebosContext) error { + + img, err := os.OpenFile(i.ImageName, os.O_WRONLY|os.O_CREATE, 0666) + if err != nil { + return fmt.Errorf("Couldn't open image file: %v", err) + } + + err = img.Truncate(i.size) + if err != nil { + return fmt.Errorf("Couldn't resize image file: %v", err) + } + + img.Close() + + loop, err := exec.Command("losetup", "-f", "--show", i.ImageName).Output() + if err != nil { + return fmt.Errorf("Failed to setup loop device") + } + context.Image = strings.TrimSpace(string(loop[:])) + i.usingLoop = true + + return nil +} + +func (i ImagePartitionAction) Run(context *debos.DebosContext) error { + i.LogStart() + + command := []string{"parted", "-s", context.Image, "mklabel", i.PartitionType} + if len(i.GptGap) > 0 { + command = append(command, i.GptGap) + } + err := debos.Command{}.Run("parted", command...) + if err != nil { + return err + } + for idx, _ := range i.Partitions { + p := &i.Partitions[idx] + var name string + if i.PartitionType == "gpt" { + name = p.Name + } else { + name = "primary" + } + + command := []string{"parted", "-a", "none", "-s", "--", context.Image, "mkpart", name} + switch p.FS { + case "vfat": + command = append(command, "fat32") + case "none": + default: + command = append(command, p.FS) + } + command = append(command, p.Start, p.End) + + err = debos.Command{}.Run("parted", command...) + if err != nil { + return err + } + + if p.Flags != nil { + for _, flag := range p.Flags { + err = debos.Command{}.Run("parted", "parted", "-s", context.Image, "set", + fmt.Sprintf("%d", p.number), flag, "on") + if err != nil { + return err + } + } + } + + devicePath := i.getPartitionDevice(p.number, *context) + // Give a chance for udevd to create proper symlinks + err = debos.Command{}.Run("udevadm", "udevadm", "settle", "-t", "5", + "-E", devicePath) + if err != nil { + return err + } + + err = i.formatPartition(p, *context) + if err != nil { + return err + } + + context.ImagePartitions = append(context.ImagePartitions, + debos.Partition{p.Name, devicePath}) + } + + context.ImageMntDir = path.Join(context.Scratchdir, "mnt") + os.MkdirAll(context.ImageMntDir, 0755) + for _, m := range i.Mountpoints { + dev := i.getPartitionDevice(m.part.number, *context) + mntpath := path.Join(context.ImageMntDir, m.Mountpoint) + os.MkdirAll(mntpath, 0755) + err := syscall.Mount(dev, mntpath, m.part.FS, 0, "") + if err != nil { + return fmt.Errorf("%s mount failed: %v", m.part.Name, err) + } + } + + err = i.generateFSTab(context) + if err != nil { + return err + } + + err = i.generateKernelRoot(context) + if err != nil { + return err + } + + return nil +} + +func (i ImagePartitionAction) Cleanup(context debos.DebosContext) error { + for idx := len(i.Mountpoints) - 1; idx >= 0; idx-- { + m := i.Mountpoints[idx] + mntpath := path.Join(context.ImageMntDir, m.Mountpoint) + syscall.Unmount(mntpath, 0) + } + + if i.usingLoop { + exec.Command("losetup", "-d", context.Image).Run() + } + + return nil +} + +func (i *ImagePartitionAction) Verify(context *debos.DebosContext) error { + if len(i.GptGap) > 0 { + log.Println("WARNING: special version of parted is needed for 'gpt_gap' option") + if i.PartitionType != "gpt" { + return fmt.Errorf("gpt_gap property could be used only with 'gpt' label") + } + // Just check if it contains correct value + _, err := units.FromHumanSize(i.GptGap) + if err != nil { + return fmt.Errorf("Failed to parse GPT offset: %s", i.GptGap) + } + } + + num := 1 + for idx, _ := range i.Partitions { + p := &i.Partitions[idx] + p.number = num + num++ + if p.Name == "" { + return fmt.Errorf("Partition without a name") + } + if p.Start == "" { + return fmt.Errorf("Partition %s missing start", p.Name) + } + if p.End == "" { + return fmt.Errorf("Partition %s missing end", p.Name) + } + + switch p.FS { + case "fat32": + p.FS = "vfat" + case "": + return fmt.Errorf("Partition %s missing fs type", p.Name) + } + } + + for idx, _ := range i.Mountpoints { + m := &i.Mountpoints[idx] + for pidx, _ := range i.Partitions { + p := &i.Partitions[pidx] + if m.Partition == p.Name { + m.part = p + break + } + } + if m.part == nil { + return fmt.Errorf("Couldn't fount partition for %s", m.Mountpoint) + } + } + + size, err := units.FromHumanSize(i.ImageSize) + if err != nil { + return fmt.Errorf("Failed to parse image size: %s", i.ImageSize) + } + + i.size = size + return nil +} |