diff --git a/docs/external-disk.md b/docs/external-disk.md index 5716e178d..508ff7241 100644 --- a/docs/external-disk.md +++ b/docs/external-disk.md @@ -45,9 +45,18 @@ onboot: command: ["/usr/bin/format", "-type", "ext4", "-label", "DATA", "/dev/vda"] ``` -`-type` can be used to specify the type. This is `ext4` by default but `btrfs` and `xfs` are also supported -`-label` can be used to give the disk a label -The final (optional) argument specifies the device name +``` +onboot: + - name: format + image: linuxkit/format: + command: ["/usr/bin/format", "-force", "-type", "xfs", "-label", "DATA", "-verbose", "/dev/vda"] +``` + +- `-force` can be used to force the partition to be cleared and recreated (if applicable), and the recreated partition formatted. This option would be used to re-init the partition on every boot, rather than persisting the partition between boots. +- `-label` can be used to give the disk a label +- `-type` can be used to specify the type. This is `ext4` by default but `btrfs` and `xfs` are also supported +- `-verbose` enables verbose logging, which can be used to troubleshoot device auto-detection and (re-)partitioning +- The final (optional) argument specifies the device name ## Mount the disk diff --git a/pkg/format/Dockerfile b/pkg/format/Dockerfile index 2d7764c1a..dbbbe83e0 100644 --- a/pkg/format/Dockerfile +++ b/pkg/format/Dockerfile @@ -1,4 +1,4 @@ -FROM linuxkit/alpine:87a0cd10449d72f374f950004467737dbf440630 AS mirror +FROM linuxkit/alpine:d1778ee29f11475548f0e861f57005a84e82ba4e AS mirror RUN mkdir -p /out/etc/apk && cp -r /etc/apk/* /out/etc/apk/ RUN apk add --no-cache --initdb -p /out \ @@ -11,10 +11,11 @@ RUN apk add --no-cache --initdb -p /out \ musl \ sfdisk \ util-linux \ + blkid \ && true RUN rm -rf /out/etc/apk /out/lib/apk /out/var/cache -FROM linuxkit/alpine:87a0cd10449d72f374f950004467737dbf440630 AS build +FROM linuxkit/alpine:d1778ee29f11475548f0e861f57005a84e82ba4e AS build RUN apk add --no-cache go musl-dev ENV GOPATH=/go PATH=$PATH:/go/bin diff --git a/pkg/format/format.go b/pkg/format/format.go index 443f035d8..c967ecb22 100644 --- a/pkg/format/format.go +++ b/pkg/format/format.go @@ -9,7 +9,6 @@ import ( "os/exec" "path/filepath" "regexp" - "sort" "strings" "syscall" "time" @@ -21,36 +20,127 @@ const ( ) var ( - labelVar string - fsTypeVar string - drives map[string]bool - driveKeys []string + labelVar string + fsTypeVar string + forceVar bool + verboseVar bool + drives map[string]bool + driveKeys []string ) +func hasPartitions(d string) bool { + err := exec.Command("sfdisk", "-d", d).Run() + return err == nil +} + +func isEmptyDevice(d string) (bool, error) { + // default result + isEmpty := false + + if verboseVar { + log.Printf("Checking if %s is empty", d) + } + + out, err := exec.Command("blkid", d).Output() + if err == nil { + log.Printf("%s has content. blkid returned: %s", d, out) + // there is content, so exit early + return false, nil + } + + if exiterr, ok := err.(*exec.ExitError); ok { + if status, ok := exiterr.Sys().(syscall.WaitStatus); ok { + // blkid exitcode 2 (from the non-busybox version) signifies the block device has no detectable content signatures + if status.ExitStatus() == 2 { + if verboseVar { + log.Printf("blkid did not find any existing content on %s.", d) + } + // no content detected, but continue through all checks + isEmpty = true + } else { + return isEmpty, fmt.Errorf("Could not determine if %s was empty. blkid %s returned: %s (exitcode %d)", d, d, out, status.ExitStatus()) + } + } + } + + if hasPartitions(d) { + log.Printf("Partition table found on device %s. Skipping.", d) + return false, nil + } + + // return final result + return isEmpty, nil +} + func autoformat(label, fsType string) error { var first string for _, d := range driveKeys { - err := exec.Command("sfdisk", "-d", d).Run() - if err == nil { - log.Printf("Partition table found on device %s. Skipping.", d) - continue + if verboseVar { + log.Printf("Considering auto format for device %s", d) + } + // break the loop with the first empty device we find + isEmpty, err := isEmptyDevice(d) + if err != nil { + return err + } + if isEmpty == true { + first = d + break } - first = d - break } if first == "" { return fmt.Errorf("No eligible disks found") } - if err := format(first, label, fsType); err != nil { - return err + return format(first, label, fsType, false) +} + +func refreshDevicesAndWaitFor(awaitedDevice string) error { + exec.Command("mdev", "-s").Run() + + // wait for device + var done bool + for i := 0; i < timeout; i++ { + stat, err := os.Stat(awaitedDevice) + if err != nil { + return err + } + if isBlockDevice(&stat) { + done = true + break + } + time.Sleep(100 * time.Millisecond) + exec.Command("mdev", "-s").Run() } + if !done { + return fmt.Errorf("Error waiting for device %s", awaitedDevice) + } + // even after the device appears we still have a race + time.Sleep(1 * time.Second) return nil } -func format(d, label, fsType string) error { +func format(d, label, fsType string, forced bool) error { + if forced { + // clear partitions on device if forced format and they exist + if hasPartitions(d) { + if verboseVar { + log.Printf("Clearing partitions on %s because forced format was requested", d) + } + partCmd := exec.Command("sfdisk", "--quiet", "--delete", d) + partCmd.Stdin = strings.NewReader(";") + if out, err := partCmd.CombinedOutput(); err != nil { + return fmt.Errorf("Error deleting partitions with sfdisk: %v\n%s", err, out) + } + } else { + if verboseVar { + log.Printf("No need to clear partitions.") + } + } + } + log.Printf("Creating partition on %s", d) /* new disks do not have an DOS signature in sector 0 this makes sfdisk complain. We can workaround this by letting @@ -75,28 +165,10 @@ func format(d, label, fsType string) error { return fmt.Errorf("Error running blockdev: %v", err) } - exec.Command("mdev", "-s").Run() - partition := fmt.Sprintf("%s1", d) - // wait for device - var done bool - for i := 0; i < timeout; i++ { - stat, err := os.Stat(partition) - 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 err := refreshDevicesAndWaitFor(partition); err != nil { + return err } - if !done { - return fmt.Errorf("Error waiting for device %s", partition) - } - // even after the device appears we still have a race - time.Sleep(1 * time.Second) // mkfs mkfsArgs := []string{"-t", fsType} @@ -133,6 +205,13 @@ func format(d, label, fsType string) error { return nil } +func isBlockDevice(d *os.FileInfo) bool { + // 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 + return (mode & syscall.S_IFMT) == syscall.S_IFBLK +} + // return a list of all available drives func findDrives() { drives = make(map[string]bool) @@ -140,45 +219,76 @@ func findDrives() { 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 { + if isBlockDevice(&d) { + if verboseVar { + log.Printf("/dev/%s is a block device", d.Name()) + } + } else { + if verboseVar { + log.Printf("/dev/%s is not a block device", d.Name()) + } continue } // ignore if it matches regexp if ignoreExp.MatchString(d.Name()) { + if verboseVar { + log.Printf("ignored device /dev/%s during drive autodetection", d.Name()) + } continue } driveKeys = append(driveKeys, filepath.Join("/dev", d.Name())) } - sort.Strings(driveKeys) - for _, d := range driveKeys { - drives[d] = true - } } func init() { + flag.BoolVar(&forceVar, "force", false, "Force format of specified single device (default false)") flag.StringVar(&labelVar, "label", "", "Disk label to apply") flag.StringVar(&fsTypeVar, "type", "ext4", "Type of filesystem to create") + flag.BoolVar(&verboseVar, "verbose", false, "Enable verbose output (default false)") +} + +func verifyBlockDevice(device string) error { + d, err := os.Stat(device) + if os.IsNotExist(err) { + return fmt.Errorf("%s does not exist", device) + } + if !isBlockDevice(&d) { + return fmt.Errorf("%s is not a block device", device) + } + // passed checks + return nil } func main() { flag.Parse() - findDrives() - if flag.NArg() > 1 { log.Fatalf("Too many arguments provided") } if flag.NArg() == 0 { + // auto-detect drives if a device to format is not explicitly specified + findDrives() if err := autoformat(labelVar, fsTypeVar); err != nil { log.Fatalf("%v", err) } } else { - if err := format(flag.Args()[0], labelVar, fsTypeVar); err != nil { + candidateDevice := flag.Args()[0] + + if err := verifyBlockDevice(candidateDevice); err != nil { log.Fatalf("%v", err) } + + if forceVar == true { + if err := format(candidateDevice, labelVar, fsTypeVar, forceVar); err != nil { + log.Fatalf("%v", err) + } + } else { + // add the deviceVar to the array of devices to consider autoformatting + driveKeys = []string{candidateDevice} + if err := autoformat(labelVar, fsTypeVar); err != nil { + log.Fatalf("%v", err) + } + } } } diff --git a/test/cases/040_packages/006_format_mount/005_by_device_force/check.sh b/test/cases/040_packages/006_format_mount/005_by_device_force/check.sh new file mode 100755 index 000000000..52f29297f --- /dev/null +++ b/test/cases/040_packages/006_format_mount/005_by_device_force/check.sh @@ -0,0 +1,54 @@ +#!/bin/sh + +function failed { + printf "format_force test suite FAILED\n" >&1 + exit 1 +} + +# sda should have been partitioned and sda1 formatted as ext4 +# command: ["/usr/bin/format", "-verbose", "-type", "ext4", "/dev/sda"] + +# sdb should have been partitioned and sdb1 formatted as ext4 +# command: ["/usr/bin/format", "-verbose", "-type", "ext4", "/dev/sdb"] + +# sda1 should remain ext4, as the format was not re-forced +# command: ["/usr/bin/format", "-verbose", "-type", "xfs", "/dev/sda"] + +# sdb should have been re-partitioned, with sdb1 now formatted as xfs due to -force flag +# command: ["/usr/bin/format", "-verbose", "-force", "-type", "xfs", "/dev/sdb"] + +ATTEMPT=0 + +while true; do + ATTEMPT=$((ATTEMPT+1)) + + echo "=== forcing device discovery (attempt ${ATTEMPT}) ===" + mdev -s + + echo "=== /dev list (attempt ${ATTEMPT}) ===" + ls -al /dev + + if [ -b /dev/sda1 ] && [ -b /dev/sdb1 ]; then + echo 'Found /dev/sda1 and /dev/sdb1 block devices' + break + fi + + if [ $ATTEMPT -ge 10 ]; then + echo "Did not detect /dev/sda1 nor /dev/sdb1 in ${ATTEMPT} attempts" + failed + fi + + sleep 1 +done + +echo "=== /dev/sda1 ===" +blkid -o export /dev/sda1 +echo "=== /dev/sdb1 ===" +blkid -o export /dev/sdb1 + +echo "=== /dev/sda1 test ===" +blkid -o export /dev/sda1 | grep -Fq 'TYPE=ext4' || failed +echo "=== /dev/sdb1 test ===" +blkid -o export /dev/sdb1 | grep -Fq 'TYPE=xfs' || failed + +printf "format_force test suite PASSED\n" >&1 diff --git a/test/cases/040_packages/006_format_mount/005_by_device_force/test.sh b/test/cases/040_packages/006_format_mount/005_by_device_force/test.sh new file mode 100755 index 000000000..41fa9db78 --- /dev/null +++ b/test/cases/040_packages/006_format_mount/005_by_device_force/test.sh @@ -0,0 +1,26 @@ +#!/bin/sh +# SUMMARY: Check that the format and mount packages work +# LABELS: +# REPEAT: + +set -e + +# Source libraries. Uncomment if needed/defined +#. "${RT_LIB}" +. "${RT_PROJECT_ROOT}/_lib/lib.sh" + +NAME=test-format +DISK1=disk1.img +DISK2=disk2.img + +clean_up() { + rm -rf ${NAME}-* ${DISK1} ${DISK2} +} +trap clean_up EXIT + +moby build -format kernel+initrd -name ${NAME} test.yml +RESULT="$(linuxkit run -disk file=${DISK1},size=512M -disk file=${DISK2},size=512M ${NAME})" +echo "${RESULT}" +echo "${RESULT}" | grep -q "suite PASSED" + +exit 0 diff --git a/test/cases/040_packages/006_format_mount/005_by_device_force/test.yml b/test/cases/040_packages/006_format_mount/005_by_device_force/test.yml new file mode 100644 index 000000000..40a790fa8 --- /dev/null +++ b/test/cases/040_packages/006_format_mount/005_by_device_force/test.yml @@ -0,0 +1,37 @@ +kernel: + image: linuxkit/kernel:4.9.51 + cmdline: "console=ttyS0" +init: + - linuxkit/init:6fe9d31a53bbd200183bb31edd795305e868d5a7 + - linuxkit/runc:a1b564248a0d0b118c11e61db9f84ecf41dd2d2a +onboot: + - name: format + image: linuxkit/format:test + command: ["/usr/bin/format", "-verbose", "-type", "ext4", "/dev/sda"] + - name: format + image: linuxkit/format:test + command: ["/usr/bin/format", "-verbose", "-type", "ext4", "/dev/sdb"] + - name: format + image: linuxkit/format:test + command: ["/usr/bin/format", "-verbose", "-type", "xfs", "/dev/sda"] + - name: format + image: linuxkit/format:test + command: ["/usr/bin/format", "-verbose", "-force", "-type", "xfs", "/dev/sdb"] + - name: test + image: linuxkit/format:test + binds: + - /check.sh:/check.sh + command: ["sh", "./check.sh"] + capabilities: + - CAP_SYS_ADMIN + - CAP_MKNOD + - name: poweroff + image: linuxkit/poweroff:1e9876c682c74d0602b7647c628bb0875fb13998 + command: ["/bin/sh", "/poweroff.sh", "10"] +files: + - path: check.sh + source: ./check.sh +trust: + org: + - linuxkit + - library