cli_tools/common/imagefile/qemu_img.go (134 lines of code) (raw):
// Copyright 2020 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package imagefile
import (
"context"
"crypto/md5"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"os/exec"
"strings"
daisy "github.com/GoogleCloudPlatform/compute-daisy"
"github.com/GoogleCloudPlatform/compute-image-tools/cli_tools/common/utils/files"
pathutils "github.com/GoogleCloudPlatform/compute-image-tools/cli_tools/common/utils/path"
"github.com/GoogleCloudPlatform/compute-image-tools/cli_tools/common/utils/shell"
)
// FormatUnknown means that qemu-img could not determine the file's format.
const FormatUnknown string = "unknown"
// The output of `qemu-img --help` contains this list.
var qemuImgFormats = strings.Split("blkdebug blklogwrites blkreplay blkverify bochs cloop "+
"copy-on-read dmg file ftp ftps gluster host_cdrom host_device http "+
"https iscsi iser luks nbd nfs null-aio null-co nvme parallels qcow "+
"qcow2 qed quorum raw rbd replication sheepdog ssh throttle vdi vhdx vmdk vpc vvfat", " ")
// ImageInfo includes metadata returned by `qemu-img info`.
type ImageInfo struct {
Format string
ActualSizeBytes int64
VirtualSizeBytes int64
// This checksum is calculated from the partial disk content extracted by QEMU.
Checksum string
}
// InfoClient runs `qemu-img info` and returns the results.
type InfoClient interface {
GetInfo(ctx context.Context, filename string) (ImageInfo, error)
}
// NewInfoClient returns a new instance of InfoClient.
func NewInfoClient() InfoClient {
return defaultInfoClient{shell.NewShellExecutor(), "out" + pathutils.RandString(5)}
}
type defaultInfoClient struct {
shellExecutor shell.Executor
tmpOutFilePrefix string
}
type fileInfoJSONTemplate struct {
Filename string `json:"filename"`
Format string `json:"format"`
ActualSizeBytes int64 `json:"actual-size"`
VirtualSizeBytes int64 `json:"virtual-size"`
}
func (client defaultInfoClient) GetInfo(ctx context.Context, filename string) (info ImageInfo, err error) {
if !files.Exists(filename) {
err = fmt.Errorf("file %q not found", filename)
return
}
jsonTemplate, err := client.getFileInfo(ctx, filename)
if err != nil {
err = daisy.Errf("Failed to inspect file %v: %v", filename, err)
return
}
info.Format = lookupFileFormat(jsonTemplate.Format)
info.ActualSizeBytes = jsonTemplate.ActualSizeBytes
info.VirtualSizeBytes = jsonTemplate.VirtualSizeBytes
checksum, err := client.getFileChecksum(ctx, filename, info.VirtualSizeBytes)
if err != nil {
err = daisy.Errf("Failed to calculate file '%v' checksum by qemu: %v", filename, err)
return
}
info.Checksum = checksum
return
}
func (client defaultInfoClient) getFileInfo(ctx context.Context, filename string) (*fileInfoJSONTemplate, error) {
cmd := exec.CommandContext(ctx, "qemu-img", "info", "--output=json", filename)
out, err := cmd.Output()
err = constructCmdErr(string(out), err, "inspection failure")
if err != nil {
return nil, err
}
jsonTemplate := fileInfoJSONTemplate{}
if err = json.Unmarshal(out, &jsonTemplate); err != nil {
return nil, daisy.Errf("failed to inspect %q: %w", filename, err)
}
return &jsonTemplate, err
}
func (client defaultInfoClient) getFileChecksum(ctx context.Context, filename string, virtualSizeBytes int64) (checksum string, err error) {
// We calculate 4 chunks' checksum. Each of them is 100MB: 0~100MB, 0.9GB~1GB, 9.9GB~10GB, the last 100MB.
// It is align with what we did for "daisy_workflows/image_import/import_image.sh" so that we can compare them.
// Each block size is 512 Bytes. So, we need to check 20000 blocks: 200000 * 512 Bytes = 100MB
// "skips" is also the start point of each chunks.
checkBlockCount := int64(200000)
blockSize := int64(512)
totalBlockCount := virtualSizeBytes / blockSize
skips := []int64{0, int64(2000000) - checkBlockCount, int64(20000000) - checkBlockCount, totalBlockCount - checkBlockCount}
for i, skip := range skips {
tmpOutFileName := fmt.Sprintf("%v%v", client.tmpOutFilePrefix, i)
defer os.Remove(tmpOutFileName)
if skip < 0 {
skip = 0
}
// Write 100MB data to a file.
var out string
out, err = client.shellExecutor.Exec("qemu-img", "dd", fmt.Sprintf("if=%v", filename),
fmt.Sprintf("of=%v", tmpOutFileName), fmt.Sprintf("bs=%v", blockSize),
fmt.Sprintf("count=%v", skip+checkBlockCount), fmt.Sprintf("skip=%v", skip))
err = constructCmdErr(out, err, "inspection for checksum failure")
if err != nil {
return
}
// Calculate checksum for the 100MB file.
f, fileErr := os.Open(tmpOutFileName)
if fileErr != nil {
err = daisy.Errf("Failed to open file '%v' for QEMU md5 checksum calculation: %v", tmpOutFileName, fileErr)
return
}
defer f.Close()
h := md5.New()
if _, md5Err := io.Copy(h, f); md5Err != nil {
err = daisy.Errf("Failed to copy data from file '%v' for QEMU md5 checksum calculation: %v", tmpOutFileName, md5Err)
return
}
newChecksum := fmt.Sprintf("%x", h.Sum(nil))
if checksum != "" {
checksum += "-"
}
checksum += newChecksum
}
return
}
func constructCmdErr(out string, err error, errorFormat string) error {
if err == nil {
return nil
}
var exitError *exec.ExitError
if errors.As(err, &exitError) {
return daisy.Errf("%v: '%w', stderr: '%s', out: '%s'", errorFormat, err, exitError.Stderr, out)
}
return daisy.Errf("%v: '%w', out: '%s'", errorFormat, err, out)
}
func lookupFileFormat(s string) string {
lower := strings.ToLower(s)
for _, format := range qemuImgFormats {
if format == lower {
return format
}
}
return FormatUnknown
}