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
}