blob: 72e133a70a4cbd2737c6cbc5516373c271c93596 [file] [log] [blame]
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// The docker2boot command converts a Docker image into a bootable GCE
// VM image.
package main
import (
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"strconv"
"strings"
"time"
)
var (
numGB = flag.Int("gb", 2, "size of raw disk, in gigabytes")
rawFile = flag.String("disk", "disk.raw", "temporary raw disk file to create and delete")
img = flag.String("image", "", "Docker image to convert. Required.")
outFile = flag.String("out", "image.tar.gz", "GCE output .tar.gz image file to create")
justRaw = flag.Bool("justraw", false, "If true, stop after preparing the raw file, but before creating the tar.gz")
)
// This is a Linux kernel and initrd that boots on GCE. It's the
// standard one that comes with the GCE Debian image.
const (
bootTarURL = "https://storage.googleapis.com/go-builder-data/boot-linux-3.16-0.bpo.3-amd64.tar.gz"
// bootUUID is the filesystem UUID in the bootTarURL snapshot.
// TODO(bradfitz): parse this out of boot/grub/grub.cfg
// instead, or write that file completely, so this doesn't
// need to exist and stay in sync with the kernel snapshot.
bootUUID = "906181f7-4e10-4a4e-8fd8-43b20ec980ff"
)
func main() {
flag.Parse()
defer os.Exit(1) // otherwise we call os.Exit(0) at the bottom
if runtime.GOOS != "linux" {
failf("docker2boot only runs on Linux")
}
if *img == "" {
failf("Missing required --image Docker image flag.")
}
if *outFile == "" {
failf("Missing required --out flag")
}
if strings.Contains(slurpFile("/proc/mounts"), "nbd0p1") {
failf("/proc/mounts shows nbd0p1 already mounted. Unmount that first.")
}
checkDeps()
mntDir, err := ioutil.TempDir("", "docker2boot")
if err != nil {
failf("Failed to create mount temp dir: %v", err)
}
defer os.RemoveAll(mntDir)
out, err := exec.Command("docker", "run", "-d", *img, "/bin/true").CombinedOutput()
if err != nil {
failf("Error creating container to snapshot: %v, %s", err, out)
}
container := strings.TrimSpace(string(out))
if os.Getenv("USER") != "root" {
failf("this tool requires root. Re-run with sudo.")
}
// Install the kernel's network block device driver, if it's not already.
// The qemu-nbd command would probably do this too, but this is a good place
// to fail early if it's not available.
run("modprobe", "nbd")
if strings.Contains(slurpFile("/proc/partitions"), "nbd0") {
// TODO(bradfitz): make the nbd device configurable,
// or auto-select a free one. Hard-coding the first
// one is lazy, but works. Who uses NBD anyway?
failf("Looks like /dev/nbd0 is already in use. Maybe a previous run failed in the middle? Try sudo qemu-nbd -d /dev/nbd0")
}
if _, err := os.Stat(*rawFile); !os.IsNotExist(err) {
failf("File %s already exists. Delete it and try again, or use a different --disk flag value.", *rawFile)
}
defer os.Remove(*rawFile)
// Make a big empty file full of zeros. Using fallocate to make a sparse
// file is much quicker (~immediate) than using dd to write from /dev/zero.
// GCE requires disk images to be sized by the gigabyte.
run("fallocate", "-l", strconv.Itoa(*numGB)+"G", *rawFile)
// Start a NBD server so the kernel's /dev/nbd0 reads/writes
// from our disk image, currently all zeros.
run("qemu-nbd", "-c", "/dev/nbd0", "--format=raw", *rawFile)
defer exec.Command("qemu-nbd", "-d", "/dev/nbd0").Run()
// Put a MS-DOS partition table on it (GCE requirement), with
// the first partition's initial sector far enough in to leave
// room for the grub boot loader.
fdisk := exec.Command("/sbin/fdisk", "/dev/nbd0")
fdisk.Stdin = strings.NewReader("o\nn\np\n1\n2048\n\nw\n")
out, err = fdisk.CombinedOutput()
if err != nil {
failf("fdisk: %v, %s", err, out)
}
// Wait for the kernel to notice the partition. fdisk does an ioctl
// to make the kernel rescan for partitions.
deadline := time.Now().Add(5 * time.Second)
for !strings.Contains(slurpFile("/proc/partitions"), "nbd0p1") {
if time.Now().After(deadline) {
failf("timeout waiting for nbd0p1 to appear")
}
time.Sleep(50 * time.Millisecond)
}
// Now that the partition is available, make a filesystem on it.
run("mkfs.ext4", "/dev/nbd0p1")
run("mount", "/dev/nbd0p1", mntDir)
defer exec.Command("umount", mntDir).Run()
log.Printf("Populating /boot/ partition from %s", bootTarURL)
pipeInto(httpGet(bootTarURL), "tar", "-zx", "-C", mntDir)
log.Printf("Exporting Docker container %s into fs", container)
exp := exec.Command("docker", "export", container)
tarPipe, err := exp.StdoutPipe()
if err != nil {
failf("Pipe: %v", err)
}
if err := exp.Start(); err != nil {
failf("docker export: %v", err)
}
pipeInto(tarPipe, "tar", "-x", "-C", mntDir)
if err := exp.Wait(); err != nil {
failf("docker export: %v", err)
}
// Docker normally provides these etc files, so they're not in
// the export and we have to include them ourselves.
writeFile(filepath.Join(mntDir, "etc", "hosts"), "127.0.0.1\tlocalhost\n")
writeFile(filepath.Join(mntDir, "etc", "resolv.conf"), "nameserver 8.8.8.8\n")
// Append the source image id & docker version to /etc/issue.
issue, err := ioutil.ReadFile("/etc/issue")
if err != nil && !os.IsNotExist(err) {
failf("Failed to read /etc/issue: %v", err)
}
out, err = exec.Command("docker", "inspect", "-f", "{{.Id}}", *img).CombinedOutput()
if err != nil {
failf("Error getting image id: %v, %s", err, out)
}
id := strings.TrimSpace(string(out))
out, err = exec.Command("docker", "-v").CombinedOutput()
if err != nil {
failf("Error getting docker version: %v, %s", err, out)
}
dockerVersion := strings.TrimSpace(string(out))
d2bissue := fmt.Sprintf("%s\nPrepared by docker2boot\nSource Docker image: %s %s\n%s\n", issue, *img, id, dockerVersion)
writeFile(filepath.Join(mntDir, "etc", "issue"), d2bissue)
// Install grub. Adjust the grub.cfg to have the correct
// filesystem UUID of the filesystem made above.
fsUUID := filesystemUUID()
grubCfgFile := filepath.Join(mntDir, "boot/grub/grub.cfg")
writeFile(grubCfgFile, strings.Replace(slurpFile(grubCfgFile), bootUUID, fsUUID, -1))
run("rm", filepath.Join(mntDir, "boot/grub/device.map"))
run("grub-install", "--boot-directory="+filepath.Join(mntDir, "boot"), "/dev/nbd0")
fstabFile := filepath.Join(mntDir, "etc/fstab")
writeFile(fstabFile, fmt.Sprintf("UUID=%s / ext4 errors=remount-ro 0 1", fsUUID))
// Set some password for testing.
run("chroot", mntDir, "/bin/bash", "-c", "echo root:r | chpasswd")
run("umount", mntDir)
run("qemu-nbd", "-d", "/dev/nbd0")
if *justRaw {
log.Printf("Stopping, and leaving %s alone.\nRun with:\n\n$ qemu-system-x86_64 -machine accel=kvm -nographic -curses -nodefconfig -smp 2 -drive if=virtio,file=%s -net nic,model=virtio -net user -boot once=d\n\n", *rawFile, *rawFile)
os.Exit(0)
}
// Write out a sparse tarball. GCE creates images from sparse
// tarballs on Google Cloud Storage.
run("tar", "-Szcf", *outFile, *rawFile)
os.Remove(*rawFile)
os.Exit(0)
}
func checkDeps() {
var missing []string
for _, cmd := range []string{
"docker",
"dumpe2fs",
"fallocate",
"grub-install",
"mkfs.ext4",
"modprobe",
"mount",
"qemu-nbd",
"rm",
"tar",
"umount",
} {
if _, err := exec.LookPath(cmd); err != nil {
missing = append(missing, cmd)
}
}
if len(missing) > 0 {
failf("Missing dependency programs: %v", missing)
}
}
func filesystemUUID() string {
e2fs, err := exec.Command("dumpe2fs", "/dev/nbd0p1").Output()
if err != nil {
failf("dumpe2fs: %v", err)
}
m := regexp.MustCompile(`Filesystem UUID:\s+(\S+)`).FindStringSubmatch(string(e2fs))
if m == nil || m[1] == "" {
failf("failed to find filesystem UUID")
}
return m[1]
}
// failf is like log.Fatalf, but runs deferred functions.
func failf(msg string, args ...interface{}) {
log.Printf(msg, args...)
runtime.Goexit()
}
func httpGet(u string) io.Reader {
res, err := http.Get(u)
if err != nil {
failf("Get %s: %v", u, err)
}
if res.StatusCode != 200 {
failf("Get %s: %v", u, res.Status)
}
// Yeah, not closing it. This program is short-lived.
return res.Body
}
func slurpFile(file string) string {
v, err := ioutil.ReadFile(file)
if err != nil {
failf("Failed to read %s: %v", file, err)
}
return string(v)
}
func writeFile(file, contents string) {
if err := ioutil.WriteFile(file, []byte(contents), 0644); err != nil {
failf("writeFile %s: %v", file, err)
}
}
func run(cmd string, args ...string) {
log.Printf("Running %s %s", cmd, args)
out, err := exec.Command(cmd, args...).CombinedOutput()
if err != nil {
failf("Error running %s %v: %v, %s", cmd, args, err, out)
}
}
func pipeInto(stdin io.Reader, cmd string, args ...string) {
log.Printf("Running %s %s", cmd, args)
c := exec.Command(cmd, args...)
c.Stdin = stdin
out, err := c.CombinedOutput()
if err != nil {
failf("Error running %s %v: %v, %s", cmd, args, err, out)
}
}