-
Notifications
You must be signed in to change notification settings - Fork 160
/
Copy pathdocker2boot.go
283 lines (248 loc) · 8.84 KB
/
docker2boot.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
// 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"
"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 := os.MkdirTemp("", "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 := os.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 := os.ReadFile(file)
if err != nil {
failf("Failed to read %s: %v", file, err)
}
return string(v)
}
func writeFile(file, contents string) {
if err := os.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)
}
}