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, &params.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, &params.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  }