github.com/mwhudson/juju@v0.0.0-20160512215208-90ff01f3497f/apiserver/images.go (about)

     1  // Copyright 2014 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package apiserver
     5  
     6  import (
     7  	"crypto/sha256"
     8  	"fmt"
     9  	"io"
    10  	"io/ioutil"
    11  	"net/http"
    12  	"path"
    13  	"path/filepath"
    14  	"strings"
    15  
    16  	"github.com/juju/errors"
    17  	"github.com/juju/utils/fslock"
    18  
    19  	"github.com/juju/juju/apiserver/params"
    20  	"github.com/juju/juju/container"
    21  	"github.com/juju/juju/instance"
    22  	"github.com/juju/juju/state"
    23  	"github.com/juju/juju/state/imagestorage"
    24  )
    25  
    26  // imagesDownloadHandler handles image download through HTTPS in the API server.
    27  type imagesDownloadHandler struct {
    28  	ctxt    httpContext
    29  	dataDir string
    30  	state   *state.State
    31  }
    32  
    33  func (h *imagesDownloadHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    34  	st, err := h.ctxt.stateForRequestUnauthenticated(r)
    35  	if err != nil {
    36  		sendError(w, err)
    37  		return
    38  	}
    39  	switch r.Method {
    40  	case "GET":
    41  		err := h.processGet(r, w, st)
    42  		if err != nil {
    43  			logger.Errorf("GET(%s) failed: %v", r.URL, err)
    44  			sendError(w, err)
    45  			return
    46  		}
    47  	default:
    48  		sendError(w, errors.MethodNotAllowedf("unsupported method: %q", r.Method))
    49  	}
    50  }
    51  
    52  // processGet handles an image GET request.
    53  func (h *imagesDownloadHandler) processGet(r *http.Request, resp http.ResponseWriter, st *state.State) error {
    54  	// Get the parameters from the query.
    55  	kind := r.URL.Query().Get(":kind")
    56  	series := r.URL.Query().Get(":series")
    57  	arch := r.URL.Query().Get(":arch")
    58  	modelUUID := r.URL.Query().Get(":modeluuid")
    59  
    60  	// Get the image details from storage.
    61  	metadata, imageReader, err := h.loadImage(st, modelUUID, kind, series, arch)
    62  	if err != nil {
    63  		return errors.Annotate(err, "error getting image from storage")
    64  	}
    65  	defer imageReader.Close()
    66  
    67  	// Stream the image to the caller.
    68  	logger.Debugf("streaming image from state blobstore: %+v", metadata)
    69  	resp.Header().Set("Content-Type", "application/x-tar-gz")
    70  	resp.Header().Set("Digest", params.EncodeChecksum(metadata.SHA256))
    71  	resp.Header().Set("Content-Length", fmt.Sprint(metadata.Size))
    72  	resp.WriteHeader(http.StatusOK)
    73  	if _, err := io.Copy(resp, imageReader); err != nil {
    74  		return errors.Annotate(err, "while streaming image")
    75  	}
    76  	return nil
    77  }
    78  
    79  // loadImage loads an os image from the blobstore,
    80  // downloading and caching it if necessary.
    81  func (h *imagesDownloadHandler) loadImage(st *state.State, modeluuid, kind, series, arch string) (
    82  	*imagestorage.Metadata, io.ReadCloser, error,
    83  ) {
    84  	// We want to ensure that if an image needs to be downloaded and cached,
    85  	// this only happens once.
    86  	imageIdent := fmt.Sprintf("image-%s-%s-%s-%s", modeluuid, kind, series, arch)
    87  	lockDir := filepath.Join(h.dataDir, "locks")
    88  	lock, err := fslock.NewLock(lockDir, imageIdent, fslock.Defaults())
    89  	if err != nil {
    90  		return nil, nil, errors.Trace(err)
    91  	}
    92  	lock.Lock("fetch and cache image " + imageIdent)
    93  	defer lock.Unlock()
    94  	storage := st.ImageStorage()
    95  	metadata, imageReader, err := storage.Image(kind, series, arch)
    96  	// Not in storage, so go fetch it.
    97  	if errors.IsNotFound(err) {
    98  		if err := h.fetchAndCacheLxcImage(storage, modeluuid, series, arch); err != nil {
    99  			return nil, nil, errors.Annotate(err, "error fetching and caching image")
   100  		}
   101  		err = networkOperationWitDefaultRetries(func() error {
   102  			metadata, imageReader, err = storage.Image(string(instance.LXC), series, arch)
   103  			return err
   104  		}, "streaming os image from blobstore")()
   105  	}
   106  	if err != nil {
   107  		return nil, nil, errors.Trace(err)
   108  	}
   109  	return metadata, imageReader, nil
   110  }
   111  
   112  // fetchAndCacheLxcImage fetches an lxc image tarball from http://cloud-images.ubuntu.com
   113  // and caches it in the state blobstore.
   114  func (h *imagesDownloadHandler) fetchAndCacheLxcImage(storage imagestorage.Storage, modeluuid, series, arch string) error {
   115  	cfg, err := h.state.ModelConfig()
   116  	if err != nil {
   117  		return errors.Trace(err)
   118  	}
   119  	imageURL, err := container.ImageDownloadURL(instance.LXC, series, arch, cfg.ImageStream(), cfg.CloudImageBaseURL())
   120  	if err != nil {
   121  		return errors.Annotatef(err, "cannot determine LXC image URL: %v", err)
   122  	}
   123  
   124  	// Fetch the image checksum.
   125  	imageFilename := path.Base(imageURL)
   126  	shafile := strings.Replace(imageURL, imageFilename, "SHA256SUMS", -1)
   127  	shaResp, err := http.Get(shafile)
   128  	if err != nil {
   129  		return errors.Annotatef(err, "cannot get sha256 data from %v", shafile)
   130  	}
   131  	defer shaResp.Body.Close()
   132  	shaInfo, err := ioutil.ReadAll(shaResp.Body)
   133  	if err != nil {
   134  		return errors.Annotatef(err, "cannot read sha256 data from %v", shafile)
   135  	}
   136  
   137  	// The sha file has lines like:
   138  	// "<checksum> *<imageFilename>"
   139  	checksum := ""
   140  	for _, line := range strings.Split(string(shaInfo), "\n") {
   141  		parts := strings.Split(line, "*")
   142  		if len(parts) != 2 {
   143  			continue
   144  		}
   145  		if parts[1] == imageFilename {
   146  			checksum = strings.TrimSpace(parts[0])
   147  			break
   148  		}
   149  	}
   150  	if checksum == "" {
   151  		return errors.Errorf("cannot find sha256 checksum for %v", imageFilename)
   152  	}
   153  
   154  	// Fetch the image.
   155  	logger.Debugf("fetching LXC image from: %v", imageURL)
   156  	resp, err := http.Get(imageURL)
   157  	if err != nil {
   158  		return errors.Annotatef(err, "cannot get image from %v", imageURL)
   159  	}
   160  	logger.Debugf("lxc image has size: %v bytes", resp.ContentLength)
   161  	defer resp.Body.Close()
   162  
   163  	hash := sha256.New()
   164  	// Set up a chain of readers to pull in the data and calculate the checksum.
   165  	rdr := io.TeeReader(resp.Body, hash)
   166  
   167  	metadata := &imagestorage.Metadata{
   168  		ModelUUID: modeluuid,
   169  		Kind:      string(instance.LXC),
   170  		Series:    series,
   171  		Arch:      arch,
   172  		Size:      resp.ContentLength,
   173  		SHA256:    checksum,
   174  		SourceURL: imageURL,
   175  	}
   176  
   177  	// Stream the image to storage.
   178  	err = networkOperationWitDefaultRetries(func() error {
   179  		return storage.AddImage(rdr, metadata)
   180  	}, "add os image to blobstore")()
   181  	if err != nil {
   182  		return errors.Trace(err)
   183  	}
   184  	// Better check the downloaded image checksum.
   185  	downloadChecksum := fmt.Sprintf("%x", hash.Sum(nil))
   186  	if downloadChecksum != checksum {
   187  		err := networkOperationWitDefaultRetries(func() error {
   188  			return storage.DeleteImage(metadata)
   189  		}, "delete os image from blobstore")()
   190  		if err != nil {
   191  			logger.Errorf("checksum mismatch, failed to delete image from storage: %v", err)
   192  		}
   193  		return errors.Errorf("download checksum mismatch %s != %s", downloadChecksum, checksum)
   194  	}
   195  	return nil
   196  }