func buildImage( log *logrus.Entry, inputFile, outputFile string, fromImage, novnc bool, boot, cdrom string, size int, ) error { // Find absolute outputFile outputFile, err := filepath.Abs(outputFile) if err != nil { log.Error("Failed to resolve output file, error: ", err) return err } // Create temp folder for the image tempFolder, err := ioutil.TempDir("", "taskcluster-worker-build-image-") if err != nil { log.Error("Failed to create temporary folder, error: ", err) return err } defer os.RemoveAll(tempFolder) var img *image.MutableImage if !fromImage { // Read machine definition machine, err2 := vm.LoadMachine(inputFile) if err2 != nil { log.Error("Failed to load machine file from ", inputFile, " error: ", err2) return err2 } // Construct MutableImage log.Info("Creating MutableImage") img, err2 = image.NewMutableImage(tempFolder, int(size), machine) if err2 != nil { log.Error("Failed to create image, error: ", err2) return err2 } } else { img, err = image.NewMutableImageFromFile(inputFile, tempFolder) if err != nil { log.Error("Failed to load image, error: ", err) return err } } // Create temp folder for sockets socketFolder, err := ioutil.TempDir("", "taskcluster-worker-sockets-") if err != nil { log.Error("Failed to create temporary folder, error: ", err) return err } defer os.RemoveAll(socketFolder) // Setup a user-space network log.Info("Creating user-space network") net, err := network.NewUserNetwork(tempFolder) if err != nil { log.Error("Failed to create user-space network, error: ", err) return err } // Setup logService so that logs can be posted to meta-service at: // http://169.254.169.254/v1/log net.SetHandler(&logService{Destination: os.Stdout}) // Create virtual machine log.Info("Creating virtual machine") vm, err := vm.NewVirtualMachine(img.Machine().Options(), img, net, socketFolder, boot, cdrom, log.WithField("component", "vm")) if err != nil { log.Error("Failed to recreated virtual-machine, error: ", err) return err } // Start the virtual machine log.Info("Starting virtual machine") vm.Start() // Open VNC display if !novnc { go qemurun.StartVNCViewer(vm.VNCSocket(), vm.Done) } // Wait for interrupt to gracefully kill everything interrupted := make(chan os.Signal, 1) signal.Notify(interrupted, os.Interrupt) // Wait for virtual machine to be done, or we get interrupted select { case <-interrupted: vm.Kill() err = errors.New("SIGINT received, aborting virtual machine") case <-vm.Done: err = vm.Error } <-vm.Done signal.Stop(interrupted) defer img.Dispose() if err != nil { if e, ok := err.(*exec.ExitError); ok { log.Error("QEMU error: ", string(e.Stderr)) } log.Info("Error running virtual machine: ", err) return err } // Package up the finished image log.Info("Package virtual machine image") err = img.Package(outputFile) if err != nil { log.Error("Failed to package finished image, error: ", err) return err } return nil }
// extractImage will extract the "disk.img", "layer.qcow2" and "machine.json" // files from a tar archive using GNU tar ensuring that sparse entries will be // extracted as sparse files. // // This also validates that files aren't symlinks and are in correct format, // with legal backing_file parameters. // // Returns a MalformedPayloadError if we believe extraction failed due to a // badly formatted image. func extractImage(imageFile, imageFolder string) (*vm.Machine, error) { // Restrict file to some maximum size if !ioext.IsPlainFile(imageFile) { return nil, fmt.Errorf("extractImage: imageFile is not a file") } if !ioext.IsFileLessThan(imageFile, maxImageSize) { return nil, engines.NewMalformedPayloadError("Image file is larger than ", maxImageSize, " bytes") } // Using zstd | tar so we get sparse files (sh to get OS pipes) tar := exec.Command("sh", "-fec", "zstd -dqc '"+imageFile+"' | "+ "tar -xoC '"+imageFolder+"' --no-same-permissions -- "+ "disk.img layer.qcow2 machine.json", ) _, err := tar.Output() if err != nil { if ee, ok := err.(*exec.ExitError); ok { return nil, engines.NewMalformedPayloadError( "Failed to extract image archieve, error: ", string(ee.Stderr), ) } // If this wasn't GNU tar exiting non-zero then it must be some internal // error. Perhaps tar is missing from the PATH. return nil, fmt.Errorf("Failed to extract image archieve, error: %s", err) } // Check files exist, are plain files and not larger than maxImageSize for _, name := range []string{"disk.img", "layer.qcow2", "machine.json"} { f := filepath.Join(imageFolder, name) if !ioext.IsPlainFile(f) { return nil, engines.NewMalformedPayloadError("Image file is missing '", name, "'") } if !ioext.IsFileLessThan(f, maxImageSize) { return nil, engines.NewMalformedPayloadError("Image file contains '", name, "' larger than ", maxImageSize, " bytes") } } // Load the machine configuration machineFile := filepath.Join(imageFolder, "machine.json") machine, err := vm.LoadMachine(machineFile) if err != nil { return nil, err } // Inspect the raw disk file diskFile := filepath.Join(imageFolder, "disk.img") diskInfo := inspectImageFile(diskFile, imageRawFormat) if diskInfo == nil || diskInfo.Format != formatRaw { return nil, engines.NewMalformedPayloadError("Image file contains ", "'disk.img' which is not a RAW image file") } if diskInfo.VirtualSize > maxImageSize { return nil, engines.NewMalformedPayloadError("Image file contains ", "'disk.img' has virtual size larger than ", maxImageSize, " bytes") } if diskInfo.DirtyFlag { return nil, engines.NewMalformedPayloadError("Image file contains ", "'disk.img' which has the dirty-flag set") } if diskInfo.BackingFile != "" { return nil, engines.NewMalformedPayloadError("Image file contains ", "'disk.img' which has a backing file, this is not permitted") } // Inspect the QCOW2 layer file layerFile := filepath.Join(imageFolder, "layer.qcow2") layerInfo := inspectImageFile(layerFile, imageQCOW2Format) if layerInfo == nil || layerInfo.Format != formatQCOW2 { return nil, engines.NewMalformedPayloadError("Image file contains ", "'layer.qcow2' which is not a QCOW2 file") } if layerInfo.VirtualSize > maxImageSize { return nil, engines.NewMalformedPayloadError("Image file contains ", "'layer.qcow2' has virtual size larger than ", maxImageSize, " bytes") } if layerInfo.DirtyFlag { return nil, engines.NewMalformedPayloadError("Image file contains ", "'layer.qcow2' which has the dirty-flag set") } if layerInfo.BackingFile != "disk.img" { return nil, engines.NewMalformedPayloadError("Image file contains ", "'layer.qcow2' which has a backing file that isn't: 'disk.img'") } if layerInfo.BackingFormat != formatRaw { return nil, engines.NewMalformedPayloadError("Image file contains ", "'layer.qcow2' which has a backing file format that isn't 'raw'") } return machine, nil }