Merge pull request #2152 from dave-tucker/multi-mount

Improve mount and format packages
This commit is contained in:
Rolf Neugebauer
2017-07-26 14:39:28 +01:00
committed by GitHub
59 changed files with 1618 additions and 222 deletions

33
pkg/extend/Dockerfile Normal file
View File

@@ -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"}'

4
pkg/extend/Makefile Normal file
View File

@@ -0,0 +1,4 @@
IMAGE=extend
DEPS=extend.go
include ../package.mk

290
pkg/extend/extend.go Normal file
View File

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

View File

@@ -1,4 +1,4 @@
FROM linuxkit/alpine:9bcf61f605ef0ce36cc94d59b8eac307862de6e1 AS mirror
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 \
@@ -6,17 +6,27 @@ RUN apk add --no-cache --initdb -p /out \
busybox \
e2fsprogs \
e2fsprogs-extra \
jq \
btrfs-progs \
xfsprogs \
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 format.go /go/src/format/
RUN go-compile.sh /go/src/format
FROM scratch
ENTRYPOINT []
CMD []
WORKDIR /
COPY --from=mirror /out/ /
COPY format.sh /
CMD ["/bin/sh", "/format.sh"]
COPY --from=build /go/bin/format usr/bin/format
CMD ["/usr/bin/format"]
LABEL org.mobyproject.config='{"binds": ["/dev:/dev"], "capabilities": ["CAP_SYS_ADMIN", "CAP_MKNOD"], "net": "new", "ipc": "new"}'

View File

@@ -1,4 +1,4 @@
IMAGE=format
DEPS=format.sh
DEPS=format.go
include ../package.mk

184
pkg/format/format.go Normal file
View File

@@ -0,0 +1,184 @@
package main
import (
"flag"
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
"path/filepath"
"regexp"
"sort"
"strings"
"syscall"
"time"
)
const (
timeout = 60
ext4opts = "resize_inode,has_journal,extent,huge_file,flex_bg,uninit_bg,64bit,dir_nlink,extra_isize"
)
var (
labelVar string
fsTypeVar string
drives map[string]bool
driveKeys []string
)
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
}
first = d
break
}
if first == "" {
return fmt.Errorf("No eligible disks found")
}
if err := format(first, label, fsType); err != nil {
return err
}
return nil
}
func format(d, label, fsType string) error {
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
fdisk create that DOS signature, by just do a "w", a write.
http://bugs.alpinelinux.org/issues/145
*/
fdiskCmd := exec.Command("fdisk", d)
fdiskCmd.Stdin = strings.NewReader("w")
if out, err := fdiskCmd.CombinedOutput(); err != nil {
return fmt.Errorf("Error running fdisk: %v\n%s", err, out)
}
// format one large partition
partCmd := exec.Command("sfdisk", "--quiet", d)
partCmd.Stdin = strings.NewReader(";")
if out, err := partCmd.CombinedOutput(); err != nil {
return fmt.Errorf("Error running sfdisk: %v\n%s", err, out)
}
// 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()
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 !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}
switch fsType {
case "ext4":
ext4Args := []string{"-F", "-O", ext4opts}
if label != "" {
ext4Args = append(ext4Args, []string{"-L", label}...)
}
mkfsArgs = append(mkfsArgs, ext4Args...)
case "btrfs":
btrfsArgs := []string{"-f"}
if label != "" {
btrfsArgs = append(btrfsArgs, []string{"-l", label}...)
}
mkfsArgs = append(mkfsArgs, btrfsArgs...)
case "xfs":
xfsArgs := []string{"-f"}
if label != "" {
xfsArgs = append(xfsArgs, []string{"-L", label}...)
}
mkfsArgs = append(mkfsArgs, xfsArgs...)
default:
log.Println("WARNING: Unsupported filesystem.")
}
mkfsArgs = append(mkfsArgs, partition)
if out, err := exec.Command("mkfs", mkfsArgs...).CombinedOutput(); err != nil {
return fmt.Errorf("Error running mkfs: %v\n%s", err, string(out))
}
log.Printf("Partition %s successfully created!", partition)
return nil
}
// return a list of all available drives
func findDrives() {
drives = make(map[string]bool)
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)
for _, d := range driveKeys {
drives[d] = true
}
}
func init() {
flag.StringVar(&labelVar, "label", "", "Disk label to apply")
flag.StringVar(&fsTypeVar, "type", "ext4", "Type of filesystem to create")
}
func main() {
flag.Parse()
findDrives()
if flag.NArg() > 1 {
log.Fatalf("Too many arguments provided")
}
if flag.NArg() == 0 {
if err := autoformat(labelVar, fsTypeVar); err != nil {
log.Fatalf("%v", err)
}
} else {
if err := format(flag.Args()[0], labelVar, fsTypeVar); err != nil {
log.Fatalf("%v", err)
}
}
}

View File

@@ -1,116 +0,0 @@
#!/bin/sh
# this script assumes anything on the disk can be removed if corrupted
# other use cases may need different scripts.
# currently only supports ext4 but should be expanded
do_fsck()
{
# preen
/sbin/e2fsck -p $*
EXIT_CODE=$?
# exit code 1 is errors corrected
[ "${EXIT_CODE}" -eq 1 ] && EXIT_CODE=0
# exit code 2 or 3 means need to reboot
[ "${EXIT_CODE}" -eq 2 -o "${EXIT_CODE}" -eq 3 ] && /sbin/reboot
# exit code 4 or over is fatal
[ "${EXIT_CODE}" -lt 4 ] && return "${EXIT_CODE}"
# try harder
/sbin/e2fsck -y $*
# exit code 1 is errors corrected
[ "${EXIT_CODE}" -eq 1 ] && EXIT_CODE=0
# exit code 2 or 3 means need to reboot
[ "${EXIT_CODE}" -eq 2 -o "${EXIT_CODE}" -eq 3 ] && /sbin/reboot
# exit code 4 or over is fatal
[ "${EXIT_CODE}" -ge 4 ] && printf "Filesystem unrecoverably corrupted, will reformat\n"
return "${EXIT_CODE}"
}
do_fsck_extend_mount()
{
DRIVE="$1"
DATA="$2"
do_fsck "$DATA" || return 1
# only try to extend if there is a single partition and free space
PARTITIONS=$(sfdisk -J "$DRIVE" | jq '.partitiontable.partitions | length')
if [ "$PARTITIONS" -eq 1 ] && \
sfdisk -F "$DRIVE" | grep -q 'Unpartitioned space' &&
! sfdisk -F "$DRIVE" | grep -q '0 B, 0 bytes, 0 sectors'
then
SPACE=$(sfdisk -F "$DRIVE" | grep 'Unpartitioned space')
printf "Resizing disk partition: $SPACE\n"
# 83 is Linux partition id
START=$(sfdisk -J "$DRIVE" | jq -e '.partitiontable.partitions | map(select(.type=="83")) | .[0].start')
sfdisk -q --delete "$DRIVE" 2> /dev/null
echo "${START},,83;" | sfdisk -q "$DRIVE"
# set bootable flag
sfdisk -A "$DRIVE" 1
# update status
blockdev --rereadpt $diskdev 2> /dev/null
mdev -s
# wait for device
for i in $(seq 1 50); do test -b "$DATA" && break || sleep .1; mdev -s; done
# resize2fs fails unless we use -f here
do_fsck -f "$DATA" || return 1
resize2fs "$DATA"
do_fsck "$DATA" || return 1
fi
}
do_mkfs()
{
diskdev="$1"
# new disks does not have an DOS signature in sector 0
# this makes sfdisk complain. We can workaround this by letting
# fdisk create that DOS signature, by just do a "w", a write.
# http://bugs.alpinelinux.org/issues/145
echo "w" | fdisk $diskdev >/dev/null
# format one large partition
echo ";" | sfdisk --quiet $diskdev
# update status
blockdev --rereadpt $diskdev 2> /dev/null
# wait for device
for i in $(seq 1 50); do test -b "$DATA" && break || sleep .1; mdev -s; done
FSOPTS="-O resize_inode,has_journal,extent,huge_file,flex_bg,uninit_bg,64bit,dir_nlink,extra_isize"
mkfs.ext4 -q -F $FSOPTS ${diskdev}1
}
# TODO fix for multiple disks, cdroms etc
DEV="$(find /dev -maxdepth 1 -type b ! -name 'loop*' | grep -v '[0-9]$' | sed 's@.*/dev/@@' | sort | head -1 )"
[ -z "${DEV}" ] && exit 1
DRIVE="/dev/${DEV}"
# see if it has a partition table already
if sfdisk -d "${DRIVE}" >/dev/null 2>/dev/null
then
DATA=$(sfdisk -J "$DRIVE" | jq -e -r '.partitiontable.partitions | map(select(.type=="83")) | .[0].node')
if [ $? -eq 0 ]
then
do_fsck_extend_mount "$DRIVE" "$DATA" || do_mkfs "$DRIVE"
else
do_mkfs "$DRIVE"
fi
else
do_mkfs "$DRIVE"
fi

View File

@@ -1,20 +1,27 @@
FROM linuxkit/alpine:9bcf61f605ef0ce36cc94d59b8eac307862de6e1 AS mirror
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 \
jq \
musl \
sfdisk \
&& 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 *.go /go/src/mountie/
RUN go-compile.sh /go/src/mountie
FROM scratch
ENTRYPOINT []
CMD []
WORKDIR /
COPY --from=mirror /out/ /
COPY mount.sh /
CMD ["/bin/sh", "/mount.sh"]
LABEL org.mobyproject.config='{"binds": ["/dev:/dev", "/var:/var:rshared,rbind"], "capabilities": ["CAP_SYS_ADMIN"], "rootfsPropagation": "shared", "net": "new", "ipc": "new"}'
COPY --from=build /go/bin/mountie usr/bin/mountie
CMD ["/usr/bin/mountie"]
LABEL org.mobyproject.config='{"binds": ["/dev:/dev", "/var:/var:rshared,rbind", "/:/hostroot"], "capabilities": ["CAP_SYS_ADMIN"], "rootfsPropagation": "shared", "net": "new", "ipc": "new"}'

View File

@@ -1,4 +1,4 @@
IMAGE=mount
DEPS=mount.sh
DEPS=mountie.go
include ../package.mk

View File

@@ -1,33 +0,0 @@
#!/bin/sh
MOUNTPOINT="$1"
[ -z "$MOUNTPOINT" ] && echo "No mountpoint specified" && exit 1
mkdir -p "$MOUNTPOINT"
mount_drive()
{
# TODO fix for multiple disks, cdroms etc
DEVS="$(find /dev -maxdepth 1 -type b ! -name 'loop*' ! -name 'nbd*' | grep -v '[0-9]$' | sed 's@.*/dev/@@' | sort)"
for DEV in $DEVS
do
DRIVE="/dev/${DEV}"
# see if it has a partition table
if sfdisk -d "${DRIVE}" >/dev/null 2>/dev/null
then
# 83 is Linux partition identifier
DATA=$(sfdisk -J "$DRIVE" | jq -e -r '.partitiontable.partitions | map(select(.type=="83")) | .[0].node')
if [ $? -eq 0 ]
then
mount "$DATA" "$MOUNTPOINT" && return
fi
fi
done
echo "WARNING: Failed to mount a persistent volume (is there one?)"
}
mount_drive

192
pkg/mount/mountie.go Normal file
View File

@@ -0,0 +1,192 @@
package main
import (
"bufio"
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
"path/filepath"
"regexp"
"sort"
"strings"
"syscall"
)
var (
deviceVar, labelVar, uuidVar 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 []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"`
}
} `json:"partitionTable"`
}
// mount drive/partition to mountpoint
func mount(device, mountpoint string) error {
if out, err := exec.Command("mount", device, mountpoint).CombinedOutput(); err != nil {
return fmt.Errorf("Error mounting %s to %s: %v\n%s", device, mountpoint, err, string(out))
}
return nil
}
func findDevice(pattern string) (string, error) {
out, err := exec.Command("findfs", pattern).Output()
if err != nil {
return "", fmt.Errorf("Error finding device with %s: %v", pattern, err)
}
device := strings.TrimSpace(string(out))
return device, nil
}
func findFirst(drives []string) (string, error) {
var first string
out, err := exec.Command("mount").Output()
if err != nil {
return "", err
}
mounted := make(map[string]bool)
scanner := bufio.NewScanner(strings.NewReader(string(out)))
for scanner.Scan() {
parts := strings.Split(scanner.Text(), " ")
if _, err := os.Stat(parts[0]); os.IsNotExist(err) {
continue
}
if _, ok := mounted[parts[0]]; !ok {
mounted[parts[0]] = true
}
}
for _, d := range drives {
err := exec.Command("sfdisk", "-d", d).Run()
if err != nil {
log.Printf("No partition table found on device %s. Skipping.", d)
continue
}
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)
}
for _, partition := range f.PartitionTable.Partitions {
// ignore anything that isn't a Linux partition
if partition.Type != "83" {
continue
}
if _, ok := mounted[partition.Node]; ok {
log.Printf("%s already mounted. Skipping", partition.Node)
continue
}
first = partition.Node
break
}
}
if first == "" {
return "", fmt.Errorf("No eligible disks found")
}
return first, nil
}
// return a list of all available drives
func findDrives() []string {
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)
return driveKeys
}
func init() {
flag.StringVar(&deviceVar, "device", "", "Name of the device to mount")
flag.StringVar(&labelVar, "label", "", "Label of the device to mount")
flag.StringVar(&uuidVar, "uuid", "", "UUID of the device to mount")
}
func main() {
flag.Parse()
var mountpoint string
switch flag.NArg() {
case 0:
log.Fatal("No mountpoints provided")
case 1:
mountpoint = flag.Args()[0]
case 2:
deviceVar = flag.Args()[0]
mountpoint = flag.Args()[1]
default:
log.Fatalf("Too many arguments")
}
err := os.MkdirAll(mountpoint, os.ModeDir)
if err != nil {
log.Fatalf("Unable to create mountpoint %s: %v", mountpoint, err)
}
if deviceVar == "" && labelVar != "" {
deviceVar, err = findDevice(fmt.Sprintf("LABEL=%s", labelVar))
if err != nil {
log.Fatal(err)
}
}
if deviceVar == "" && uuidVar != "" {
deviceVar, err = findDevice(fmt.Sprintf("UUID=%s", uuidVar))
if err != nil {
log.Fatal(err)
}
}
if deviceVar == "" {
// find first device
drives := findDrives()
first, err := findFirst(drives)
if err != nil {
log.Fatal(err)
}
deviceVar = first
}
if err := mount(deviceVar, mountpoint); err != nil {
log.Fatal(err)
}
}