format: blkid content check, format argument, and verbose argument

- Added new argument -force to the format utility. This will force formatting of the specified single device if it exists and is a block device
- By default, the format package will no longer format the specified single device if content exists on that device
- Added new blkid-based check for content, in addition to the existing check for partitions on autoformat candidate devices.
- Cleanup of old unused code.
- Refactoring of block device verification.
- Added test/cases/040_packages/006_format_mount/005_by_device_force
- Added document of new arguments to /docs/external-disk.md. Also sorted the arguments in the doc alphabetically and added them as bullets so they do not run together on the page.

Signed-off-by: Isaac Rodman <isaac@eyz.us>
This commit is contained in:
Isaac Rodman 2017-09-22 13:44:49 -07:00 committed by Isaac Rodman
parent 367c7e2c88
commit 3de78f2109
6 changed files with 287 additions and 50 deletions

View File

@ -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:<hash>
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

View File

@ -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

View File

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

View File

@ -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

View File

@ -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

View File

@ -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