github.com/mattyw/juju@v0.0.0-20140610034352-732aecd63861/state/apiserver/tools.go (about) 1 // Copyright 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package apiserver 5 6 import ( 7 "crypto/sha256" 8 "encoding/json" 9 "fmt" 10 "io" 11 "io/ioutil" 12 "net/http" 13 "os" 14 "path" 15 "strings" 16 17 "github.com/juju/juju/environs" 18 "github.com/juju/juju/environs/filestorage" 19 "github.com/juju/juju/environs/sync" 20 envtools "github.com/juju/juju/environs/tools" 21 "github.com/juju/juju/state/api/params" 22 "github.com/juju/juju/state/apiserver/common" 23 "github.com/juju/juju/tools" 24 "github.com/juju/juju/version" 25 ) 26 27 // toolsHandler handles tool upload through HTTPS in the API server. 28 type toolsHandler struct { 29 httpHandler 30 } 31 32 func (h *toolsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 33 if err := h.authenticate(r); err != nil { 34 h.authError(w, h) 35 return 36 } 37 if err := h.validateEnvironUUID(r); err != nil { 38 h.sendError(w, http.StatusNotFound, err.Error()) 39 return 40 } 41 42 switch r.Method { 43 case "POST": 44 // Add a local charm to the store provider. 45 // Requires a "series" query specifying the series to use for the charm. 46 agentTools, disableSSLHostnameVerification, err := h.processPost(r) 47 if err != nil { 48 h.sendError(w, http.StatusBadRequest, err.Error()) 49 return 50 } 51 h.sendJSON(w, http.StatusOK, ¶ms.ToolsResult{ 52 Tools: agentTools, 53 DisableSSLHostnameVerification: disableSSLHostnameVerification, 54 }) 55 default: 56 h.sendError(w, http.StatusMethodNotAllowed, fmt.Sprintf("unsupported method: %q", r.Method)) 57 } 58 } 59 60 // sendJSON sends a JSON-encoded response to the client. 61 func (h *toolsHandler) sendJSON(w http.ResponseWriter, statusCode int, response *params.ToolsResult) error { 62 w.Header().Set("Content-Type", "application/json") 63 w.WriteHeader(statusCode) 64 body, err := json.Marshal(response) 65 if err != nil { 66 return err 67 } 68 w.Write(body) 69 return nil 70 } 71 72 // sendError sends a JSON-encoded error response. 73 func (h *toolsHandler) sendError(w http.ResponseWriter, statusCode int, message string) error { 74 err := common.ServerError(fmt.Errorf(message)) 75 return h.sendJSON(w, statusCode, ¶ms.ToolsResult{Error: err}) 76 } 77 78 // processPost handles a charm upload POST request after authentication. 79 func (h *toolsHandler) processPost(r *http.Request) (*tools.Tools, bool, error) { 80 query := r.URL.Query() 81 binaryVersionParam := query.Get("binaryVersion") 82 if binaryVersionParam == "" { 83 return nil, false, fmt.Errorf("expected binaryVersion argument") 84 } 85 toolsVersion, err := version.ParseBinary(binaryVersionParam) 86 if err != nil { 87 return nil, false, fmt.Errorf("invalid tools version %q: %v", binaryVersionParam, err) 88 } 89 var fakeSeries []string 90 seriesParam := query.Get("series") 91 if seriesParam != "" { 92 fakeSeries = strings.Split(seriesParam, ",") 93 } 94 logger.Debugf("request to upload tools %s for series %q", toolsVersion, seriesParam) 95 // Make sure the content type is x-tar-gz. 96 contentType := r.Header.Get("Content-Type") 97 if contentType != "application/x-tar-gz" { 98 return nil, false, fmt.Errorf("expected Content-Type: application/x-tar-gz, got: %v", contentType) 99 } 100 return h.handleUpload(r.Body, toolsVersion, fakeSeries...) 101 } 102 103 // handleUpload uploads the tools data from the reader to env storage as the specified version. 104 func (h *toolsHandler) handleUpload(r io.Reader, toolsVersion version.Binary, fakeSeries ...string) (*tools.Tools, bool, error) { 105 // Set up a local temp directory for the tools tarball. 106 tmpDir, err := ioutil.TempDir("", "juju-upload-tools-") 107 if err != nil { 108 return nil, false, fmt.Errorf("cannot create temp dir: %v", err) 109 } 110 defer os.RemoveAll(tmpDir) 111 toolsFilename := envtools.StorageName(toolsVersion) 112 toolsDir := path.Dir(toolsFilename) 113 fullToolsDir := path.Join(tmpDir, toolsDir) 114 err = os.MkdirAll(fullToolsDir, 0700) 115 if err != nil { 116 return nil, false, fmt.Errorf("cannot create tools dir %s: %v", toolsDir, err) 117 } 118 119 // Read the tools tarball from the request, calculating the sha256 along the way. 120 fullToolsFilename := path.Join(tmpDir, toolsFilename) 121 toolsFile, err := os.Create(fullToolsFilename) 122 if err != nil { 123 return nil, false, fmt.Errorf("cannot create tools file %s: %v", fullToolsFilename, err) 124 } 125 logger.Debugf("saving uploaded tools to temp file: %s", fullToolsFilename) 126 defer toolsFile.Close() 127 sha256hash := sha256.New() 128 var size int64 129 if size, err = io.Copy(toolsFile, io.TeeReader(r, sha256hash)); err != nil { 130 return nil, false, fmt.Errorf("error processing file upload: %v", err) 131 } 132 if size == 0 { 133 return nil, false, fmt.Errorf("no tools uploaded") 134 } 135 136 // TODO(wallyworld): check integrity of tools tarball. 137 138 // Create a tools record and sync to storage. 139 uploadedTools := &tools.Tools{ 140 Version: toolsVersion, 141 Size: size, 142 SHA256: fmt.Sprintf("%x", sha256hash.Sum(nil)), 143 } 144 logger.Debugf("about to upload tools %+v to storage", uploadedTools) 145 return h.uploadToStorage(uploadedTools, tmpDir, toolsFilename, fakeSeries...) 146 } 147 148 // uploadToStorage uploads the tools from the specified directory to environment storage. 149 func (h *toolsHandler) uploadToStorage(uploadedTools *tools.Tools, toolsDir, 150 toolsFilename string, fakeSeries ...string) (*tools.Tools, bool, error) { 151 152 // SyncTools requires simplestreams metadata to find the tools to upload. 153 stor, err := filestorage.NewFileStorageWriter(toolsDir) 154 if err != nil { 155 return nil, false, fmt.Errorf("cannot create metadata storage: %v", err) 156 } 157 // Generate metadata for the fake series. The URL for each fake series 158 // record points to the same tools tarball. 159 allToolsMetadata := []*tools.Tools{uploadedTools} 160 for _, series := range fakeSeries { 161 vers := uploadedTools.Version 162 vers.Series = series 163 allToolsMetadata = append(allToolsMetadata, &tools.Tools{ 164 Version: vers, 165 URL: uploadedTools.URL, 166 Size: uploadedTools.Size, 167 SHA256: uploadedTools.SHA256, 168 }) 169 } 170 err = envtools.MergeAndWriteMetadata(stor, allToolsMetadata, false) 171 if err != nil { 172 return nil, false, fmt.Errorf("cannot get environment config: %v", err) 173 } 174 175 // Create the environment so we can get the storage to which we upload the tools. 176 envConfig, err := h.state.EnvironConfig() 177 if err != nil { 178 return nil, false, fmt.Errorf("cannot get environment config: %v", err) 179 } 180 env, err := environs.New(envConfig) 181 if err != nil { 182 return nil, false, fmt.Errorf("cannot access environment: %v", err) 183 } 184 185 // Now perform the upload. 186 builtTools := &sync.BuiltTools{ 187 Version: uploadedTools.Version, 188 Dir: toolsDir, 189 StorageName: toolsFilename, 190 Size: uploadedTools.Size, 191 Sha256Hash: uploadedTools.SHA256, 192 } 193 uploadedTools, err = sync.SyncBuiltTools(env.Storage(), builtTools, fakeSeries...) 194 if err != nil { 195 return nil, false, err 196 } 197 return uploadedTools, !envConfig.SSLHostnameVerification(), nil 198 }