golang.org/x/build@v0.0.0-20240506185731-218518f32b70/cmd/docker2boot/docker2boot.go (about) 1 // Copyright 2015 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // The docker2boot command converts a Docker image into a bootable GCE 6 // VM image. 7 package main 8 9 import ( 10 "flag" 11 "fmt" 12 "io" 13 "log" 14 "net/http" 15 "os" 16 "os/exec" 17 "path/filepath" 18 "regexp" 19 "runtime" 20 "strconv" 21 "strings" 22 "time" 23 ) 24 25 var ( 26 numGB = flag.Int("gb", 2, "size of raw disk, in gigabytes") 27 rawFile = flag.String("disk", "disk.raw", "temporary raw disk file to create and delete") 28 img = flag.String("image", "", "Docker image to convert. Required.") 29 outFile = flag.String("out", "image.tar.gz", "GCE output .tar.gz image file to create") 30 31 justRaw = flag.Bool("justraw", false, "If true, stop after preparing the raw file, but before creating the tar.gz") 32 ) 33 34 // This is a Linux kernel and initrd that boots on GCE. It's the 35 // standard one that comes with the GCE Debian image. 36 const ( 37 bootTarURL = "https://storage.googleapis.com/go-builder-data/boot-linux-3.16-0.bpo.3-amd64.tar.gz" 38 39 // bootUUID is the filesystem UUID in the bootTarURL snapshot. 40 // TODO(bradfitz): parse this out of boot/grub/grub.cfg 41 // instead, or write that file completely, so this doesn't 42 // need to exist and stay in sync with the kernel snapshot. 43 bootUUID = "906181f7-4e10-4a4e-8fd8-43b20ec980ff" 44 ) 45 46 func main() { 47 flag.Parse() 48 defer os.Exit(1) // otherwise we call os.Exit(0) at the bottom 49 if runtime.GOOS != "linux" { 50 failf("docker2boot only runs on Linux") 51 } 52 if *img == "" { 53 failf("Missing required --image Docker image flag.") 54 } 55 if *outFile == "" { 56 failf("Missing required --out flag") 57 } 58 if strings.Contains(slurpFile("/proc/mounts"), "nbd0p1") { 59 failf("/proc/mounts shows nbd0p1 already mounted. Unmount that first.") 60 } 61 62 checkDeps() 63 64 mntDir, err := os.MkdirTemp("", "docker2boot") 65 if err != nil { 66 failf("Failed to create mount temp dir: %v", err) 67 } 68 defer os.RemoveAll(mntDir) 69 70 out, err := exec.Command("docker", "run", "-d", *img, "/bin/true").CombinedOutput() 71 if err != nil { 72 failf("Error creating container to snapshot: %v, %s", err, out) 73 } 74 container := strings.TrimSpace(string(out)) 75 76 if os.Getenv("USER") != "root" { 77 failf("this tool requires root. Re-run with sudo.") 78 } 79 80 // Install the kernel's network block device driver, if it's not already. 81 // The qemu-nbd command would probably do this too, but this is a good place 82 // to fail early if it's not available. 83 run("modprobe", "nbd") 84 85 if strings.Contains(slurpFile("/proc/partitions"), "nbd0") { 86 // TODO(bradfitz): make the nbd device configurable, 87 // or auto-select a free one. Hard-coding the first 88 // one is lazy, but works. Who uses NBD anyway? 89 failf("Looks like /dev/nbd0 is already in use. Maybe a previous run failed in the middle? Try sudo qemu-nbd -d /dev/nbd0") 90 } 91 if _, err := os.Stat(*rawFile); !os.IsNotExist(err) { 92 failf("File %s already exists. Delete it and try again, or use a different --disk flag value.", *rawFile) 93 } 94 defer os.Remove(*rawFile) 95 96 // Make a big empty file full of zeros. Using fallocate to make a sparse 97 // file is much quicker (~immediate) than using dd to write from /dev/zero. 98 // GCE requires disk images to be sized by the gigabyte. 99 run("fallocate", "-l", strconv.Itoa(*numGB)+"G", *rawFile) 100 101 // Start a NBD server so the kernel's /dev/nbd0 reads/writes 102 // from our disk image, currently all zeros. 103 run("qemu-nbd", "-c", "/dev/nbd0", "--format=raw", *rawFile) 104 defer exec.Command("qemu-nbd", "-d", "/dev/nbd0").Run() 105 106 // Put a MS-DOS partition table on it (GCE requirement), with 107 // the first partition's initial sector far enough in to leave 108 // room for the grub boot loader. 109 fdisk := exec.Command("/sbin/fdisk", "/dev/nbd0") 110 fdisk.Stdin = strings.NewReader("o\nn\np\n1\n2048\n\nw\n") 111 out, err = fdisk.CombinedOutput() 112 if err != nil { 113 failf("fdisk: %v, %s", err, out) 114 } 115 116 // Wait for the kernel to notice the partition. fdisk does an ioctl 117 // to make the kernel rescan for partitions. 118 deadline := time.Now().Add(5 * time.Second) 119 for !strings.Contains(slurpFile("/proc/partitions"), "nbd0p1") { 120 if time.Now().After(deadline) { 121 failf("timeout waiting for nbd0p1 to appear") 122 } 123 time.Sleep(50 * time.Millisecond) 124 } 125 126 // Now that the partition is available, make a filesystem on it. 127 run("mkfs.ext4", "/dev/nbd0p1") 128 run("mount", "/dev/nbd0p1", mntDir) 129 defer exec.Command("umount", mntDir).Run() 130 131 log.Printf("Populating /boot/ partition from %s", bootTarURL) 132 pipeInto(httpGet(bootTarURL), "tar", "-zx", "-C", mntDir) 133 134 log.Printf("Exporting Docker container %s into fs", container) 135 exp := exec.Command("docker", "export", container) 136 tarPipe, err := exp.StdoutPipe() 137 if err != nil { 138 failf("Pipe: %v", err) 139 } 140 if err := exp.Start(); err != nil { 141 failf("docker export: %v", err) 142 } 143 pipeInto(tarPipe, "tar", "-x", "-C", mntDir) 144 if err := exp.Wait(); err != nil { 145 failf("docker export: %v", err) 146 } 147 148 // Docker normally provides these etc files, so they're not in 149 // the export and we have to include them ourselves. 150 writeFile(filepath.Join(mntDir, "etc", "hosts"), "127.0.0.1\tlocalhost\n") 151 writeFile(filepath.Join(mntDir, "etc", "resolv.conf"), "nameserver 8.8.8.8\n") 152 153 // Append the source image id & docker version to /etc/issue. 154 issue, err := os.ReadFile("/etc/issue") 155 if err != nil && !os.IsNotExist(err) { 156 failf("Failed to read /etc/issue: %v", err) 157 } 158 out, err = exec.Command("docker", "inspect", "-f", "{{.Id}}", *img).CombinedOutput() 159 if err != nil { 160 failf("Error getting image id: %v, %s", err, out) 161 } 162 id := strings.TrimSpace(string(out)) 163 out, err = exec.Command("docker", "-v").CombinedOutput() 164 if err != nil { 165 failf("Error getting docker version: %v, %s", err, out) 166 } 167 dockerVersion := strings.TrimSpace(string(out)) 168 d2bissue := fmt.Sprintf("%s\nPrepared by docker2boot\nSource Docker image: %s %s\n%s\n", issue, *img, id, dockerVersion) 169 writeFile(filepath.Join(mntDir, "etc", "issue"), d2bissue) 170 171 // Install grub. Adjust the grub.cfg to have the correct 172 // filesystem UUID of the filesystem made above. 173 fsUUID := filesystemUUID() 174 grubCfgFile := filepath.Join(mntDir, "boot/grub/grub.cfg") 175 writeFile(grubCfgFile, strings.Replace(slurpFile(grubCfgFile), bootUUID, fsUUID, -1)) 176 run("rm", filepath.Join(mntDir, "boot/grub/device.map")) 177 run("grub-install", "--boot-directory="+filepath.Join(mntDir, "boot"), "/dev/nbd0") 178 fstabFile := filepath.Join(mntDir, "etc/fstab") 179 writeFile(fstabFile, fmt.Sprintf("UUID=%s / ext4 errors=remount-ro 0 1", fsUUID)) 180 181 // Set some password for testing. 182 run("chroot", mntDir, "/bin/bash", "-c", "echo root:r | chpasswd") 183 184 run("umount", mntDir) 185 run("qemu-nbd", "-d", "/dev/nbd0") 186 if *justRaw { 187 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) 188 os.Exit(0) 189 } 190 191 // Write out a sparse tarball. GCE creates images from sparse 192 // tarballs on Google Cloud Storage. 193 run("tar", "-Szcf", *outFile, *rawFile) 194 195 os.Remove(*rawFile) 196 os.Exit(0) 197 } 198 199 func checkDeps() { 200 var missing []string 201 for _, cmd := range []string{ 202 "docker", 203 "dumpe2fs", 204 "fallocate", 205 "grub-install", 206 "mkfs.ext4", 207 "modprobe", 208 "mount", 209 "qemu-nbd", 210 "rm", 211 "tar", 212 "umount", 213 } { 214 if _, err := exec.LookPath(cmd); err != nil { 215 missing = append(missing, cmd) 216 } 217 } 218 if len(missing) > 0 { 219 failf("Missing dependency programs: %v", missing) 220 } 221 } 222 223 func filesystemUUID() string { 224 e2fs, err := exec.Command("dumpe2fs", "/dev/nbd0p1").Output() 225 if err != nil { 226 failf("dumpe2fs: %v", err) 227 } 228 m := regexp.MustCompile(`Filesystem UUID:\s+(\S+)`).FindStringSubmatch(string(e2fs)) 229 if m == nil || m[1] == "" { 230 failf("failed to find filesystem UUID") 231 } 232 return m[1] 233 } 234 235 // failf is like log.Fatalf, but runs deferred functions. 236 func failf(msg string, args ...interface{}) { 237 log.Printf(msg, args...) 238 runtime.Goexit() 239 } 240 241 func httpGet(u string) io.Reader { 242 res, err := http.Get(u) 243 if err != nil { 244 failf("Get %s: %v", u, err) 245 } 246 if res.StatusCode != 200 { 247 failf("Get %s: %v", u, res.Status) 248 } 249 // Yeah, not closing it. This program is short-lived. 250 return res.Body 251 } 252 253 func slurpFile(file string) string { 254 v, err := os.ReadFile(file) 255 if err != nil { 256 failf("Failed to read %s: %v", file, err) 257 } 258 return string(v) 259 } 260 261 func writeFile(file, contents string) { 262 if err := os.WriteFile(file, []byte(contents), 0644); err != nil { 263 failf("writeFile %s: %v", file, err) 264 } 265 } 266 267 func run(cmd string, args ...string) { 268 log.Printf("Running %s %s", cmd, args) 269 out, err := exec.Command(cmd, args...).CombinedOutput() 270 if err != nil { 271 failf("Error running %s %v: %v, %s", cmd, args, err, out) 272 } 273 } 274 275 func pipeInto(stdin io.Reader, cmd string, args ...string) { 276 log.Printf("Running %s %s", cmd, args) 277 c := exec.Command(cmd, args...) 278 c.Stdin = stdin 279 out, err := c.CombinedOutput() 280 if err != nil { 281 failf("Error running %s %v: %v, %s", cmd, args, err, out) 282 } 283 }