github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/container/kvm/sync.go (about) 1 // Copyright 2016 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package kvm 5 6 import ( 7 "crypto/sha256" 8 "fmt" 9 "io" 10 "io/ioutil" 11 "net/http" 12 "os" 13 "path" 14 "path/filepath" 15 "time" 16 17 humanize "github.com/dustin/go-humanize" 18 "github.com/juju/clock" 19 "github.com/juju/errors" 20 "github.com/juju/os/series" 21 22 "github.com/juju/juju/environs/imagedownloads" 23 "github.com/juju/juju/environs/simplestreams" 24 "github.com/juju/juju/juju/paths" 25 ) 26 27 // BIOSFType is the file type we want to fetch and use for kvm instances which 28 // boot using a legacy BIOS boot loader. 29 const BIOSFType = "disk1.img" 30 31 // UEFIFType is the file type we want to fetch and use for kvm instances which 32 // boot using UEFI. In our case this is ARM64. 33 const UEFIFType = "uefi1.img" 34 35 // Oner gets the one matching item from simplestreams. 36 type Oner interface { 37 One() (*imagedownloads.Metadata, error) 38 } 39 40 // syncParams conveys the information necessary for calling imagedownloads.One. 41 type syncParams struct { 42 arch, series, stream, ftype string 43 srcFunc func() simplestreams.DataSource 44 } 45 46 // One implements Oner. 47 func (p syncParams) One() (*imagedownloads.Metadata, error) { 48 if err := p.exists(); err != nil { 49 return nil, errors.Trace(err) 50 } 51 return imagedownloads.One(p.arch, p.series, p.stream, p.ftype, p.srcFunc) 52 } 53 54 func (p syncParams) exists() error { 55 fname := backingFileName(p.series, p.arch) 56 baseDir, err := paths.DataDir(series.MustHostSeries()) 57 if err != nil { 58 return errors.Trace(err) 59 } 60 path := filepath.Join(baseDir, kvm, guestDir, fname) 61 62 if _, err := os.Stat(path); err == nil { 63 return errors.AlreadyExistsf("%q %q image for exists at %q", p.series, p.arch, path) 64 } 65 return nil 66 } 67 68 // Validate that our types fulfull their implementations. 69 var _ Oner = (*syncParams)(nil) 70 var _ Fetcher = (*fetcher)(nil) 71 72 // Fetcher is an interface to permit faking input in tests. The default 73 // implementation is updater, defined in this file. 74 type Fetcher interface { 75 Fetch() error 76 Close() 77 } 78 79 type fetcher struct { 80 metadata *imagedownloads.Metadata 81 req *http.Request 82 client *http.Client 83 image *Image 84 } 85 86 // Fetch implements Fetcher. It fetches the image file from simplestreams and 87 // delegates writing it out and creating the qcow3 backing file to Image.write. 88 func (f *fetcher) Fetch() error { 89 resp, err := f.client.Do(f.req) 90 if err != nil { 91 return errors.Trace(err) 92 } 93 94 defer func() { 95 err = resp.Body.Close() 96 if err != nil { 97 logger.Debugf("failed defer %q", errors.Trace(err)) 98 } 99 }() 100 101 if resp.StatusCode != 200 { 102 f.image.cleanup() 103 return errors.NotFoundf( 104 "got %d fetching image %q", resp.StatusCode, path.Base( 105 f.req.URL.String())) 106 } 107 err = f.image.write(resp.Body, f.metadata) 108 if err != nil { 109 return errors.Trace(err) 110 } 111 return nil 112 } 113 114 // Close calls images cleanup method for deferred closing of the image tmpFile. 115 func (f *fetcher) Close() { 116 f.image.cleanup() 117 } 118 119 type ProgressCallback func(message string) 120 121 // Sync updates the local cached images by reading the simplestreams data and 122 // caching if an image matching the contrainsts doesn't exist. It retrieves 123 // metadata information from Oner and updates local cache via Fetcher. 124 // A ProgressCallback can optionally be passed which will get update messages 125 // as data is copied. 126 func Sync(o Oner, f Fetcher, progress ProgressCallback) error { 127 md, err := o.One() 128 if err != nil { 129 if errors.IsAlreadyExists(err) { 130 // We've already got a backing file for this series/architecture. 131 return nil 132 } 133 return errors.Trace(err) 134 } 135 if f == nil { 136 f, err = newDefaultFetcher(md, paths.DataDir, progress) 137 if err != nil { 138 return errors.Trace(err) 139 } 140 defer f.Close() 141 } 142 err = f.Fetch() 143 if err != nil { 144 return errors.Trace(err) 145 } 146 return nil 147 } 148 149 // Image represents a server image. 150 type Image struct { 151 FilePath string 152 progress ProgressCallback 153 tmpFile *os.File 154 runCmd runFunc 155 } 156 157 type progressWriter struct { 158 callback ProgressCallback 159 url string 160 total uint64 161 maxBytes uint64 162 startTime *time.Time 163 lastPercent int 164 clock clock.Clock 165 } 166 167 var _ (io.Writer) = (*progressWriter)(nil) 168 169 func (p *progressWriter) Write(content []byte) (n int, err error) { 170 if p.clock == nil { 171 p.clock = clock.WallClock 172 } 173 p.total += uint64(len(content)) 174 if p.startTime == nil { 175 now := p.clock.Now() 176 p.startTime = &now 177 return len(content), nil 178 } 179 if p.callback != nil { 180 elapsed := p.clock.Now().Sub(*p.startTime) 181 // Avoid measurements that aren't interesting 182 if elapsed > time.Millisecond { 183 percent := (float64(p.total) * 100.0) / float64(p.maxBytes) 184 intPercent := int(percent + 0.5) 185 if p.lastPercent != intPercent { 186 bps := uint64((float64(p.total) / elapsed.Seconds()) + 0.5) 187 p.callback(fmt.Sprintf("copying %s %d%% (%s/s)", p.url, intPercent, humanize.Bytes(bps))) 188 p.lastPercent = intPercent 189 } 190 } 191 } 192 return len(content), nil 193 } 194 195 // write saves the stream to disk and updates the metadata file. 196 func (i *Image) write(r io.Reader, md *imagedownloads.Metadata) error { 197 tmpPath := i.tmpFile.Name() 198 defer func() { 199 err := i.tmpFile.Close() 200 if err != nil { 201 logger.Errorf("failed to close %q %s", tmpPath, err) 202 } 203 err = os.Remove(tmpPath) 204 if err != nil { 205 logger.Errorf("failed to remove %q after use %s", tmpPath, err) 206 } 207 208 }() 209 210 hash := sha256.New() 211 var writer io.Writer 212 if i.progress == nil { 213 writer = io.MultiWriter(i.tmpFile, hash) 214 } else { 215 dlURL, _ := md.DownloadURL() 216 progWriter := &progressWriter{ 217 url: dlURL.String(), 218 callback: i.progress, 219 maxBytes: uint64(md.Size), 220 total: 0, 221 } 222 writer = io.MultiWriter(i.tmpFile, hash, progWriter) 223 } 224 _, err := io.Copy(writer, r) 225 if err != nil { 226 i.cleanup() 227 return errors.Trace(err) 228 } 229 230 result := fmt.Sprintf("%x", hash.Sum(nil)) 231 if result != md.SHA256 { 232 i.cleanup() 233 return errors.Errorf( 234 "hash sum mismatch for %s: %s != %s", i.tmpFile.Name(), result, md.SHA256) 235 } 236 237 // TODO(jam): 2017-03-19 If this is slow, maybe we want to add a progress step for it, rather than only 238 // indicating download progress. 239 output, err := i.runCmd( 240 "qemu-img", "convert", "-f", "qcow2", tmpPath, i.FilePath) 241 logger.Debugf("qemu-image convert output: %s", output) 242 if err != nil { 243 i.cleanupAll() 244 return errors.Trace(err) 245 } 246 return nil 247 } 248 249 // cleanup attempts to close and remove the tempfile download image. It can be 250 // called if things don't work out. E.g. sha256 mismatch, incorrect size... 251 func (i *Image) cleanup() { 252 if err := i.tmpFile.Close(); err != nil { 253 logger.Debugf("%s", err.Error()) 254 } 255 256 if err := os.Remove(i.tmpFile.Name()); err != nil { 257 logger.Debugf("got %q removing %q", err.Error(), i.tmpFile.Name()) 258 } 259 } 260 261 // cleanupAll cleans up the possible backing file as well. 262 func (i *Image) cleanupAll() { 263 i.cleanup() 264 err := os.Remove(i.FilePath) 265 if err != nil { 266 logger.Debugf("got %q removing %q", err.Error(), i.FilePath) 267 } 268 } 269 270 func newDefaultFetcher(md *imagedownloads.Metadata, pathfinder func(string) (string, error), callback ProgressCallback) (*fetcher, error) { 271 i, err := newImage(md, pathfinder, callback) 272 if err != nil { 273 return nil, errors.Trace(err) 274 } 275 dlURL, err := md.DownloadURL() 276 if err != nil { 277 return nil, errors.Trace(err) 278 } 279 req, err := http.NewRequest("GET", dlURL.String(), nil) 280 if err != nil { 281 return nil, errors.Trace(err) 282 } 283 client := &http.Client{} 284 return &fetcher{metadata: md, image: i, client: client, req: req}, nil 285 } 286 287 func newImage(md *imagedownloads.Metadata, pathfinder func(string) (string, error), callback ProgressCallback) (*Image, error) { 288 // Setup names and paths. 289 dlURL, err := md.DownloadURL() 290 if err != nil { 291 return nil, errors.Trace(err) 292 } 293 baseDir, err := pathfinder(series.MustHostSeries()) 294 if err != nil { 295 return nil, errors.Trace(err) 296 } 297 298 // Closing this is deferred in Image.write. 299 fh, err := ioutil.TempFile("", fmt.Sprintf("juju-kvm-%s-", path.Base(dlURL.String()))) 300 if err != nil { 301 return nil, errors.Trace(err) 302 } 303 304 return &Image{ 305 FilePath: filepath.Join( 306 baseDir, kvm, guestDir, backingFileName(md.Release, md.Arch)), 307 tmpFile: fh, 308 runCmd: run, 309 progress: callback, 310 }, nil 311 } 312 313 func backingFileName(series, arch string) string { 314 // TODO(ro) validate series and arch to be sure they are in the right order. 315 return fmt.Sprintf("%s-%s-backing-file.qcow", series, arch) 316 }