-
Notifications
You must be signed in to change notification settings - Fork 21
/
qemu_img.go
184 lines (159 loc) · 6 KB
/
qemu_img.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
// 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-import/cli_tools/common/utils/files"
pathutils "github.com/GoogleCloudPlatform/compute-image-import/cli_tools/common/utils/path"
"github.com/GoogleCloudPlatform/compute-image-import/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
}