github.com/coreos/mantle@v0.13.0/platform/qemu.go (about) 1 // Copyright 2019 Red Hat 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package platform 16 17 import ( 18 "errors" 19 "fmt" 20 "io/ioutil" 21 "os" 22 "path/filepath" 23 "regexp" 24 "runtime" 25 "strings" 26 "time" 27 28 "github.com/coreos/go-semver/semver" 29 30 "github.com/coreos/mantle/system/exec" 31 "github.com/coreos/mantle/util" 32 ) 33 34 type MachineOptions struct { 35 AdditionalDisks []Disk 36 } 37 38 type Disk struct { 39 Size string // disk image size in bytes, optional suffixes "K", "M", "G", "T" allowed. Incompatible with BackingFile 40 BackingFile string // raw disk image to use. Incompatible with Size. 41 DeviceOpts []string // extra options to pass to qemu. "serial=XXXX" makes disks show up as /dev/disk/by-id/virtio-<serial> 42 } 43 44 var ( 45 ErrNeedSizeOrFile = errors.New("Disks need either Size or BackingFile specified") 46 ErrBothSizeAndFile = errors.New("Only one of Size and BackingFile can be specified") 47 primaryDiskOptions = []string{"serial=primary-disk"} 48 ) 49 50 // Copy Container Linux input image and specialize copy for running kola tests. 51 // Return FD to the copy, which is a deleted file. 52 // This is not mandatory; the tests will do their best without it. 53 func MakeCLDiskTemplate(inputPath string) (output *os.File, result error) { 54 seterr := func(err error) { 55 if result == nil { 56 result = err 57 } 58 } 59 60 // create output file 61 outputPath, err := mkpath("/var/tmp") 62 if err != nil { 63 return nil, err 64 } 65 defer os.Remove(outputPath) 66 67 // copy file 68 // cp is used since it supports sparse and reflink. 69 cp := exec.Command("cp", "--force", 70 "--sparse=always", "--reflink=auto", 71 inputPath, outputPath) 72 cp.Stdout = os.Stdout 73 cp.Stderr = os.Stderr 74 if err := cp.Run(); err != nil { 75 return nil, fmt.Errorf("copying file: %v", err) 76 } 77 78 // create mount point 79 tmpdir, err := ioutil.TempDir("", "kola-qemu-") 80 if err != nil { 81 return nil, fmt.Errorf("making temporary directory: %v", err) 82 } 83 defer func() { 84 if err := os.Remove(tmpdir); err != nil { 85 seterr(fmt.Errorf("deleting directory %s: %v", tmpdir, err)) 86 } 87 }() 88 89 // set up loop device 90 cmd := exec.Command("losetup", "-Pf", "--show", outputPath) 91 stdout, err := cmd.StdoutPipe() 92 if err != nil { 93 return nil, fmt.Errorf("getting stdout pipe: %v", err) 94 } 95 defer stdout.Close() 96 if err := cmd.Start(); err != nil { 97 return nil, fmt.Errorf("running losetup: %v", err) 98 } 99 buf, err := ioutil.ReadAll(stdout) 100 if err != nil { 101 cmd.Wait() 102 return nil, fmt.Errorf("reading losetup output: %v", err) 103 } 104 if err := cmd.Wait(); err != nil { 105 return nil, fmt.Errorf("setting up loop device: %v", err) 106 } 107 loopdev := strings.TrimSpace(string(buf)) 108 defer func() { 109 if err := exec.Command("losetup", "-d", loopdev).Run(); err != nil { 110 seterr(fmt.Errorf("tearing down loop device: %v", err)) 111 } 112 }() 113 114 // wait for OEM block device 115 oemdev := loopdev + "p6" 116 err = util.Retry(1000, 5*time.Millisecond, func() error { 117 if _, err := os.Stat(oemdev); !os.IsNotExist(err) { 118 return nil 119 } 120 return fmt.Errorf("timed out waiting for device node; did you specify a qcow image by mistake?") 121 }) 122 if err != nil { 123 return nil, err 124 } 125 126 // mount OEM partition 127 if err := exec.Command("mount", oemdev, tmpdir).Run(); err != nil { 128 return nil, fmt.Errorf("mounting OEM partition %s on %s: %v", oemdev, tmpdir, err) 129 } 130 defer func() { 131 if err := exec.Command("umount", tmpdir).Run(); err != nil { 132 seterr(fmt.Errorf("unmounting %s: %v", tmpdir, err)) 133 } 134 }() 135 136 // write console settings to grub.cfg 137 f, err := os.OpenFile(filepath.Join(tmpdir, "grub.cfg"), os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644) 138 if err != nil { 139 return nil, fmt.Errorf("opening grub.cfg: %v", err) 140 } 141 defer f.Close() 142 if _, err = f.WriteString("set linux_console=\"console=ttyS0,115200\"\n"); err != nil { 143 return nil, fmt.Errorf("writing grub.cfg: %v", err) 144 } 145 146 // return fd to output file 147 output, err = os.Open(outputPath) 148 if err != nil { 149 return nil, fmt.Errorf("opening %v: %v", outputPath, err) 150 } 151 return 152 } 153 154 func (d Disk) getOpts() string { 155 if len(d.DeviceOpts) == 0 { 156 return "" 157 } 158 return "," + strings.Join(d.DeviceOpts, ",") 159 } 160 161 func (d Disk) setupFile() (*os.File, error) { 162 if d.Size == "" && d.BackingFile == "" { 163 return nil, ErrNeedSizeOrFile 164 } 165 if d.Size != "" && d.BackingFile != "" { 166 return nil, ErrBothSizeAndFile 167 } 168 169 if d.Size != "" { 170 return setupDisk(d.Size) 171 } else { 172 return setupDiskFromFile(d.BackingFile) 173 } 174 } 175 176 // Create a nameless temporary qcow2 image file backed by a raw image. 177 func setupDiskFromFile(imageFile string) (*os.File, error) { 178 // a relative path would be interpreted relative to /tmp 179 backingFile, err := filepath.Abs(imageFile) 180 if err != nil { 181 return nil, err 182 } 183 // Keep the COW image from breaking if the "latest" symlink changes. 184 // Ignore /proc/*/fd/* paths, since they look like symlinks but 185 // really aren't. 186 if !strings.HasPrefix(backingFile, "/proc/") { 187 backingFile, err = filepath.EvalSymlinks(backingFile) 188 if err != nil { 189 return nil, err 190 } 191 } 192 193 qcowOpts := fmt.Sprintf("backing_file=%s,lazy_refcounts=on", backingFile) 194 return setupDisk("-o", qcowOpts) 195 } 196 197 func setupDisk(additionalOptions ...string) (*os.File, error) { 198 dstFileName, err := mkpath("") 199 if err != nil { 200 return nil, err 201 } 202 defer os.Remove(dstFileName) 203 204 opts := []string{"create", "-f", "qcow2", dstFileName} 205 opts = append(opts, additionalOptions...) 206 207 qemuImg := exec.Command("qemu-img", opts...) 208 qemuImg.Stderr = os.Stderr 209 210 if err := qemuImg.Run(); err != nil { 211 return nil, err 212 } 213 214 return os.OpenFile(dstFileName, os.O_RDWR, 0) 215 } 216 217 func mkpath(basedir string) (string, error) { 218 f, err := ioutil.TempFile(basedir, "mantle-qemu") 219 if err != nil { 220 return "", err 221 } 222 defer f.Close() 223 return f.Name(), nil 224 } 225 226 func CreateQEMUCommand(board, uuid, biosImage, consolePath, confPath, diskImagePath string, isIgnition bool, options MachineOptions) ([]string, []*os.File, error) { 227 var qmCmd []string 228 229 // As we expand this list of supported native + board 230 // archs combos we should coordinate with the 231 // coreos-assembler folks as they utilize something 232 // similar in cosa run 233 var qmBinary string 234 combo := runtime.GOARCH + "--" + board 235 switch combo { 236 case "amd64--amd64-usr": 237 qmBinary = "qemu-system-x86_64" 238 qmCmd = []string{ 239 "qemu-system-x86_64", 240 "-machine", "accel=kvm", 241 "-cpu", "host", 242 "-m", "1024", 243 } 244 case "amd64--arm64-usr": 245 qmBinary = "qemu-system-aarch64" 246 qmCmd = []string{ 247 "qemu-system-aarch64", 248 "-machine", "virt", 249 "-cpu", "cortex-a57", 250 "-m", "2048", 251 } 252 case "arm64--amd64-usr": 253 qmBinary = "qemu-system-x86_64" 254 qmCmd = []string{ 255 "qemu-system-x86_64", 256 "-machine", "pc-q35-2.8", 257 "-cpu", "kvm64", 258 "-m", "1024", 259 } 260 case "arm64--arm64-usr": 261 qmBinary = "qemu-system-aarch64" 262 qmCmd = []string{ 263 "qemu-system-aarch64", 264 "-machine", "virt,accel=kvm,gic-version=3", 265 "-cpu", "host", 266 "-m", "2048", 267 } 268 default: 269 panic("host-guest combo not supported: " + combo) 270 } 271 272 qmCmd = append(qmCmd, 273 "-bios", biosImage, 274 "-smp", "1", 275 "-uuid", uuid, 276 "-display", "none", 277 "-chardev", "file,id=log,path="+consolePath, 278 "-serial", "chardev:log", 279 ) 280 281 if isIgnition { 282 qmCmd = append(qmCmd, 283 "-fw_cfg", "name=opt/com.coreos/config,file="+confPath) 284 } else { 285 qmCmd = append(qmCmd, 286 "-fsdev", "local,id=cfg,security_model=none,readonly,path="+confPath, 287 "-device", Virtio(board, "9p", "fsdev=cfg,mount_tag=config-2")) 288 } 289 290 // auto-read-only is only available in 3.1.0 & greater versions of QEMU 291 var autoReadOnly string 292 version, err := exec.Command(qmBinary, "--version").CombinedOutput() 293 if err != nil { 294 return nil, nil, fmt.Errorf("retrieving qemu version: %v", err) 295 } 296 pat := regexp.MustCompile(`version (\d+\.\d+\.\d+)`) 297 vNum := pat.FindSubmatch(version) 298 if len(vNum) < 2 { 299 return nil, nil, fmt.Errorf("unable to parse qemu version number") 300 } 301 qmSemver, err := semver.NewVersion(string(vNum[1])) 302 if err != nil { 303 return nil, nil, fmt.Errorf("parsing qemu semver: %v", err) 304 } 305 if !qmSemver.LessThan(*semver.New("3.1.0")) { 306 autoReadOnly = ",auto-read-only=off" 307 plog.Debugf("disabling auto-read-only for QEMU drives") 308 } 309 310 allDisks := append([]Disk{ 311 { 312 BackingFile: diskImagePath, 313 DeviceOpts: primaryDiskOptions, 314 }, 315 }, options.AdditionalDisks...) 316 317 var extraFiles []*os.File 318 fdnum := 3 // first additional file starts at position 3 319 fdset := 1 320 321 for _, disk := range allDisks { 322 optionsDiskFile, err := disk.setupFile() 323 if err != nil { 324 return nil, nil, err 325 } 326 //defer optionsDiskFile.Close() 327 extraFiles = append(extraFiles, optionsDiskFile) 328 329 id := fmt.Sprintf("d%d", fdnum) 330 qmCmd = append(qmCmd, "-add-fd", fmt.Sprintf("fd=%d,set=%d", fdnum, fdset), 331 "-drive", fmt.Sprintf("if=none,id=%s,format=qcow2,file=/dev/fdset/%d%s", id, fdset, autoReadOnly), 332 "-device", Virtio(board, "blk", fmt.Sprintf("drive=%s%s", id, disk.getOpts()))) 333 fdnum += 1 334 fdset += 1 335 } 336 337 return qmCmd, extraFiles, nil 338 } 339 340 // The virtio device name differs between machine types but otherwise 341 // configuration is the same. Use this to help construct device args. 342 func Virtio(board, device, args string) string { 343 var suffix string 344 switch board { 345 case "amd64-usr": 346 suffix = "pci" 347 case "arm64-usr": 348 suffix = "device" 349 default: 350 panic(board) 351 } 352 return fmt.Sprintf("virtio-%s-%s,%s", device, suffix, args) 353 }