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 }