From 8dcb57429a5a8547b4e1a7d692589210795af3a7 Mon Sep 17 00:00:00 2001
From: Dave Tucker
Date: Wed, 5 Jul 2017 22:14:03 +0100
Subject: [PATCH] pkg: Add extend for extending partitions
This was split out from pkg/format into its own package.
It has the ability to extend ext4, btrfs and xfs partitions.
Signed-off-by: Dave Tucker
---
pkg/extend/Dockerfile | 33 +++++
pkg/extend/Makefile | 4 +
pkg/extend/extend.go | 290 ++++++++++++++++++++++++++++++++++++++++++
3 files changed, 327 insertions(+)
create mode 100644 pkg/extend/Dockerfile
create mode 100644 pkg/extend/Makefile
create mode 100644 pkg/extend/extend.go
diff --git a/pkg/extend/Dockerfile b/pkg/extend/Dockerfile
new file mode 100644
index 000000000..2eda64f4e
--- /dev/null
+++ b/pkg/extend/Dockerfile
@@ -0,0 +1,33 @@
+FROM linuxkit/alpine:488aa6f5dd2d8121a3c5c5c7a1ecf97c424b96ac AS mirror
+
+RUN mkdir -p /out/etc/apk && cp -r /etc/apk/* /out/etc/apk/
+RUN apk add --no-cache --initdb -p /out \
+ alpine-baselayout \
+ busybox \
+ e2fsprogs \
+ e2fsprogs-extra \
+ btrfs-progs \
+ xfsprogs \
+ xfsprogs-extra \
+ musl \
+ sfdisk \
+ util-linux \
+ && true
+RUN rm -rf /out/etc/apk /out/lib/apk /out/var/cache
+
+FROM linuxkit/alpine:488aa6f5dd2d8121a3c5c5c7a1ecf97c424b96ac AS build
+
+RUN apk add --no-cache go musl-dev
+ENV GOPATH=/go PATH=$PATH:/go/bin
+
+COPY extend.go /go/src/extend/
+RUN go-compile.sh /go/src/extend
+
+FROM scratch
+ENTRYPOINT []
+CMD []
+WORKDIR /
+COPY --from=mirror /out/ /
+COPY --from=build /go/bin/extend usr/bin/extend
+CMD ["/usr/bin/extend"]
+LABEL org.mobyproject.config='{"binds": ["/dev:/dev"], "capabilities": ["CAP_SYS_ADMIN", "CAP_MKNOD"], "net": "new", "ipc": "new"}'
diff --git a/pkg/extend/Makefile b/pkg/extend/Makefile
new file mode 100644
index 000000000..eaa9d62b2
--- /dev/null
+++ b/pkg/extend/Makefile
@@ -0,0 +1,4 @@
+IMAGE=extend
+DEPS=extend.go
+
+include ../package.mk
diff --git a/pkg/extend/extend.go b/pkg/extend/extend.go
new file mode 100644
index 000000000..a5d489156
--- /dev/null
+++ b/pkg/extend/extend.go
@@ -0,0 +1,290 @@
+package main
+
+import (
+ "encoding/json"
+ "flag"
+ "fmt"
+ "io/ioutil"
+ "log"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "regexp"
+ "sort"
+ "strings"
+ "syscall"
+ "time"
+)
+
+const timeout = 60
+
+var (
+ fsTypeVar string
+ driveKeys []string
+)
+
+// Fdisk is the JSON output from libfdisk
+type Fdisk struct {
+ PartitionTable struct {
+ Label string `json:"label"`
+ ID string `json:"id"`
+ Device string `json:"device"`
+ Unit string `json:"unit"`
+ FirstLBA int `json:"firstlba"`
+ LastLBA int `json:"lastlba"`
+ Partitions []Partition
+ } `json:"partitionTable"`
+}
+
+// Partition represents a single partition
+type Partition struct {
+ Node string `json:"node"`
+ Start int `json:"start"`
+ Size int `json:"size"`
+ Type string `json:"type"`
+ UUID string `json:"uuid"`
+ Name string `json:"name"`
+}
+
+func autoextend(fsType string) error {
+ for _, d := range driveKeys {
+ err := exec.Command("sfdisk", "-d", d).Run()
+ if err != nil {
+ log.Printf("No partition table found on device %s. Skipping.", d)
+ continue
+ }
+ if err := extend(d, fsType); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func extend(d, fsType string) error {
+ mountpoint := "/mnt/tmp"
+
+ data, err := exec.Command("sfdisk", "-J", d).Output()
+ if err != nil {
+ log.Fatalf("Unable to get drive data for %s from sfdisk: %v", d, err)
+ }
+
+ f := Fdisk{}
+ if err := json.Unmarshal(data, &f); err != nil {
+ return fmt.Errorf("Unable to unmarshal partition table from sfdisk: %v", err)
+ }
+
+ if len(f.PartitionTable.Partitions) > 1 {
+ log.Printf("Disk %s has more than 1 partition. Skipping", d)
+ return nil
+ }
+
+ partition := f.PartitionTable.Partitions[0]
+ if partition.Type != "83" {
+ return fmt.Errorf("Partition 1 on disk %s is not a Linux Partition", d)
+ }
+
+ if partition.Start+partition.Size == f.PartitionTable.LastLBA {
+ log.Printf("No free space on device to extend partition")
+ return nil
+ }
+
+ switch fsType {
+ case "ext4":
+ if err := e2fsck(partition.Node, false); err != nil {
+ return fmt.Errorf("Initial e2fsck failed: %v", err)
+ }
+ // resize2fs fails unless we set force=true here
+ if err := e2fsck(partition.Node, true); err != nil {
+ return fmt.Errorf("e2fsck before resize failed: %v", err)
+ }
+
+ if err := createPartition(d, partition); err != nil {
+ return err
+ }
+
+ if err := exec.Command("resize2fs", partition.Node).Run(); err != nil {
+ return fmt.Errorf("Error running resize2fs: %v", err)
+ }
+
+ if err := e2fsck(partition.Node, false); err != nil {
+ return fmt.Errorf("e2fsck after resize failed: %v", err)
+ }
+ case "btrfs":
+ // We don't check btrfs before or after mount as it's less susceptible to consistency errors
+ // than it's extfs cousins.
+ if err := os.MkdirAll(mountpoint, os.ModeDir); err != nil {
+ return err
+ }
+ if err := createPartition(d, partition); err != nil {
+ return err
+ }
+ if out, err := exec.Command("mount", partition.Node, mountpoint).CombinedOutput(); err != nil {
+ return fmt.Errorf("Error mounting partition: %s", string(out))
+ }
+ if out, err := exec.Command("btrfs", "filesystem", "resize", "max", mountpoint).CombinedOutput(); err != nil {
+ return fmt.Errorf("Error resizing partition: %s\n%s", err, string(out))
+ }
+ if out, err := exec.Command("umount", mountpoint).CombinedOutput(); err != nil {
+ return fmt.Errorf("Error unmounting partition: %s", string(out))
+ }
+ case "xfs":
+ // We don't check xfs before mounting as the xfs_check or xfs_repair utilities
+ // should be used only if we suspect a file system consistency problem.
+ if err := os.MkdirAll(mountpoint, os.ModeDir); err != nil {
+ return err
+ }
+ if err := createPartition(d, partition); err != nil {
+ return err
+ }
+ if out, err := exec.Command("mount", partition.Node, mountpoint).CombinedOutput(); err != nil {
+ return fmt.Errorf("Error mounting partition: %s", string(out))
+ }
+ if out, err := exec.Command("xfs_growfs", mountpoint).CombinedOutput(); err != nil {
+ return fmt.Errorf("Error resizing partition: %s\n%s", err, string(out))
+ }
+ if out, err := exec.Command("umount", mountpoint).CombinedOutput(); err != nil {
+ return fmt.Errorf("Error unmounting partition: %s", string(out))
+ }
+ if out, err := exec.Command("xfs_repair", "-n", partition.Node).CombinedOutput(); err != nil {
+ return fmt.Errorf("Error checking filesystem: %s", string(out))
+ }
+
+ default:
+ return fmt.Errorf("%s is not a supported filesystem", fsType)
+ }
+
+ log.Printf("Successfully resized %s", d)
+ return nil
+}
+
+func createPartition(d string, partition Partition) error {
+ if err := exec.Command("sfdisk", "-q", "--delete", d).Run(); err != nil {
+ return fmt.Errorf("Error erasing partition table: %v", err.Error())
+ }
+
+ createCmd := exec.Command("sfdisk", "-q", d)
+ createCmd.Stdin = strings.NewReader(fmt.Sprintf("%d,,83;", partition.Start))
+ if err := createCmd.Run(); err != nil {
+ return fmt.Errorf("Error creating partition table: %v", err)
+ }
+
+ if err := exec.Command("sfdisk", "-A", d, "1").Run(); err != nil {
+ return fmt.Errorf("Error making %s bootable: %v", d, err)
+ }
+
+ // update status
+ if err := exec.Command("blockdev", "--rereadpt", d).Run(); err != nil {
+ return fmt.Errorf("Error running blockdev: %v", err)
+ }
+
+ exec.Command("mdev", "-s").Run()
+
+ // wait for device
+ var done bool
+ for i := 0; i < timeout; i++ {
+ stat, err := os.Stat(partition.Node)
+ if err == nil {
+ mode := stat.Sys().(*syscall.Stat_t).Mode
+ if (mode & syscall.S_IFMT) == syscall.S_IFBLK {
+ done = true
+ break
+ }
+ }
+ time.Sleep(100 * time.Millisecond)
+ exec.Command("mdev", "-s").Run()
+ }
+ if !done {
+ return fmt.Errorf("Error waiting for device %s", partition.Node)
+ }
+ // even after the device appears we still have a race
+ time.Sleep(1 * time.Second)
+ return nil
+}
+
+func e2fsck(d string, force bool) error {
+ // preen
+ args := []string{"-p"}
+ if force {
+ args = append(args, "-f")
+ }
+ args = append(args, d)
+ if err := exec.Command("e2fsck", args...).Run(); err != nil {
+ if exiterr, ok := err.(*exec.ExitError); ok {
+ status, ok := exiterr.Sys().(syscall.WaitStatus)
+ if !ok {
+ return fmt.Errorf("Unable to get status code from e2fsck")
+ }
+ switch status.ExitStatus() {
+ case 1:
+ return nil
+ case 2, 3:
+ return fmt.Errorf("e2fsck fixed errors but requires a reboot")
+ }
+ } else {
+ return fmt.Errorf("Unable to cast err to ExitError")
+ }
+ }
+
+ // exit code was > 4. try harder
+ args[0] = "-y"
+ if err := exec.Command("/sbin/e2fsck", args...).Run(); err != nil {
+ if exiterr, ok := err.(*exec.ExitError); ok {
+ status, ok := exiterr.Sys().(syscall.WaitStatus)
+ if !ok {
+ return fmt.Errorf("Unable to get status code from e2fsck")
+ }
+ switch status.ExitStatus() {
+ case 1:
+ return nil
+ case 2, 3:
+ return fmt.Errorf("e2fsck fixed errors but requires a reboot")
+ default:
+ return fmt.Errorf("e2fsck exited with fatal error: %v", err)
+ }
+ } else {
+ return fmt.Errorf("Unable to cast err to ExitError")
+ }
+ }
+ return nil
+}
+
+// return a list of all available drives
+func findDrives() {
+ driveKeys = []string{}
+ ignoreExp := regexp.MustCompile(`^loop.*$|^nbd.*$|^[a-z]+[0-9]+$`)
+ devs, _ := ioutil.ReadDir("/dev")
+ for _, d := range devs {
+ // this probably shouldn't be so hard
+ // but d.Mode()&os.ModeDevice == 0 doesn't work as expected
+ mode := d.Sys().(*syscall.Stat_t).Mode
+ if (mode & syscall.S_IFMT) != syscall.S_IFBLK {
+ continue
+ }
+ // ignore if it matches regexp
+ if ignoreExp.MatchString(d.Name()) {
+ continue
+ }
+ driveKeys = append(driveKeys, filepath.Join("/dev", d.Name()))
+ }
+ sort.Strings(driveKeys)
+}
+
+func init() {
+ flag.StringVar(&fsTypeVar, "type", "ext4", "Type of filesystem to create")
+}
+
+func main() {
+ flag.Parse()
+ findDrives()
+ if flag.NArg() == 0 {
+ if err := autoextend(fsTypeVar); err != nil {
+ log.Fatalf("%v", err)
+ }
+ } else {
+ for _, arg := range flag.Args() {
+ if err := extend(arg, fsTypeVar); err != nil {
+ log.Fatalf("%v", err)
+ }
+ }
+ }
+}