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) + } + } + } +}