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  }