launchpad.net/~rogpeppe/juju-core/500-errgo-fix@v0.0.0-20140213181702-000000002356/provider/maas/storage.go (about)

     1  // Copyright 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package maas
     5  
     6  import (
     7  	"bytes"
     8  	"encoding/base64"
     9  	"fmt"
    10  	"io"
    11  	"io/ioutil"
    12  	"net/url"
    13  	"sort"
    14  	"strings"
    15  	"sync"
    16  
    17  	"launchpad.net/gomaasapi"
    18  
    19  	"launchpad.net/juju-core/environs/storage"
    20  	"launchpad.net/juju-core/errors"
    21  	"launchpad.net/juju-core/utils"
    22  )
    23  
    24  type maasStorage struct {
    25  	// Mutex protects the "*Unlocked" fields.
    26  	sync.Mutex
    27  
    28  	// The Environ that this Storage is for.
    29  	environUnlocked *maasEnviron
    30  
    31  	// Reference to the URL on the API where files are stored.
    32  	maasClientUnlocked gomaasapi.MAASObject
    33  }
    34  
    35  var _ storage.Storage = (*maasStorage)(nil)
    36  
    37  func NewStorage(env *maasEnviron) storage.Storage {
    38  	stor := new(maasStorage)
    39  	stor.environUnlocked = env
    40  	stor.maasClientUnlocked = env.getMAASClient().GetSubObject("files")
    41  	return stor
    42  }
    43  
    44  // getSnapshot returns a consistent copy of a maasStorage.  Use this if you
    45  // need a consistent view of the object's entire state, without having to
    46  // lock the object the whole time.
    47  //
    48  // An easy mistake to make with "defer" is to keep holding a lock without
    49  // realizing it, while you go on to block on http requests or other slow
    50  // things that don't actually require the lock.  In most cases you can just
    51  // create a snapshot first (releasing the lock immediately) and then do the
    52  // rest of the work with the snapshot.
    53  func (stor *maasStorage) getSnapshot() *maasStorage {
    54  	stor.Lock()
    55  	defer stor.Unlock()
    56  
    57  	return &maasStorage{
    58  		environUnlocked:    stor.environUnlocked,
    59  		maasClientUnlocked: stor.maasClientUnlocked,
    60  	}
    61  }
    62  
    63  // addressFileObject creates a MAASObject pointing to a given file.
    64  // Takes out a lock on the storage object to get a consistent view.
    65  func (stor *maasStorage) addressFileObject(name string) gomaasapi.MAASObject {
    66  	stor.Lock()
    67  	defer stor.Unlock()
    68  	return stor.maasClientUnlocked.GetSubObject(name)
    69  }
    70  
    71  // retrieveFileObject retrieves the information of the named file, including
    72  // its download URL and its contents, as a MAASObject.
    73  //
    74  // This may return many different errors, but specifically, it returns
    75  // an error that satisfies errors.IsNotFoundError if the file did not exist.
    76  //
    77  // The function takes out a lock on the storage object.
    78  func (stor *maasStorage) retrieveFileObject(name string) (gomaasapi.MAASObject, error) {
    79  	obj, err := stor.addressFileObject(name).Get()
    80  	if err != nil {
    81  		noObj := gomaasapi.MAASObject{}
    82  		serverErr, ok := err.(gomaasapi.ServerError)
    83  		if ok && serverErr.StatusCode == 404 {
    84  			return noObj, errors.NotFoundf("file '%s' not found", name)
    85  		}
    86  		msg := fmt.Errorf("could not access file '%s': %v", name, err)
    87  		return noObj, msg
    88  	}
    89  	return obj, nil
    90  }
    91  
    92  // All filenames need to be namespaced so they are private to this environment.
    93  // This prevents different environments from interfering with each other.
    94  // We're using the agent name UUID here.
    95  func (stor *maasStorage) prefixWithPrivateNamespace(name string) string {
    96  	env := stor.getSnapshot().environUnlocked
    97  	prefix := env.ecfg().maasAgentName()
    98  	if prefix != "" {
    99  		return prefix + "-" + name
   100  	}
   101  	return name
   102  }
   103  
   104  // Get is specified in the StorageReader interface.
   105  func (stor *maasStorage) Get(name string) (io.ReadCloser, error) {
   106  	name = stor.prefixWithPrivateNamespace(name)
   107  	fileObj, err := stor.retrieveFileObject(name)
   108  	if err != nil {
   109  		return nil, err
   110  	}
   111  	data, err := fileObj.GetField("content")
   112  	if err != nil {
   113  		return nil, fmt.Errorf("could not extract file content for %s: %v", name, err)
   114  	}
   115  	buf, err := base64.StdEncoding.DecodeString(data)
   116  	if err != nil {
   117  		return nil, fmt.Errorf("bad data in file '%s': %v", name, err)
   118  	}
   119  	return ioutil.NopCloser(bytes.NewReader(buf)), nil
   120  }
   121  
   122  // extractFilenames returns the filenames from a "list" operation on the
   123  // MAAS API, sorted by name.
   124  func (stor *maasStorage) extractFilenames(listResult gomaasapi.JSONObject) ([]string, error) {
   125  	privatePrefix := stor.prefixWithPrivateNamespace("")
   126  	list, err := listResult.GetArray()
   127  	if err != nil {
   128  		return nil, err
   129  	}
   130  	result := make([]string, len(list))
   131  	for index, entry := range list {
   132  		file, err := entry.GetMap()
   133  		if err != nil {
   134  			return nil, err
   135  		}
   136  		filename, err := file["filename"].GetString()
   137  		if err != nil {
   138  			return nil, err
   139  		}
   140  		// When listing files we need to return them without our special prefix.
   141  		result[index] = strings.TrimPrefix(filename, privatePrefix)
   142  	}
   143  	sort.Strings(result)
   144  	return result, nil
   145  }
   146  
   147  // List is specified in the StorageReader interface.
   148  func (stor *maasStorage) List(prefix string) ([]string, error) {
   149  	prefix = stor.prefixWithPrivateNamespace(prefix)
   150  	params := make(url.Values)
   151  	params.Add("prefix", prefix)
   152  	snapshot := stor.getSnapshot()
   153  	obj, err := snapshot.maasClientUnlocked.CallGet("list", params)
   154  	if err != nil {
   155  		return nil, err
   156  	}
   157  	return snapshot.extractFilenames(obj)
   158  }
   159  
   160  // URL is specified in the StorageReader interface.
   161  func (stor *maasStorage) URL(name string) (string, error) {
   162  	name = stor.prefixWithPrivateNamespace(name)
   163  	fileObj, err := stor.retrieveFileObject(name)
   164  	if err != nil {
   165  		return "", err
   166  	}
   167  	uri, err := fileObj.GetField("anon_resource_uri")
   168  	if err != nil {
   169  		msg := fmt.Errorf("could not get file's download URL (may be an outdated MAAS): %s", err)
   170  		return "", msg
   171  	}
   172  
   173  	partialURL, err := url.Parse(uri)
   174  	if err != nil {
   175  		return "", err
   176  	}
   177  	fullURL := fileObj.URL().ResolveReference(partialURL)
   178  	return fullURL.String(), nil
   179  }
   180  
   181  // ConsistencyStrategy is specified in the StorageReader interface.
   182  func (stor *maasStorage) DefaultConsistencyStrategy() utils.AttemptStrategy {
   183  	// This storage backend has immediate consistency, so there's no
   184  	// need to wait.  One attempt should do.
   185  	return utils.AttemptStrategy{}
   186  }
   187  
   188  // ShouldRetry is specified in the StorageReader interface.
   189  func (stor *maasStorage) ShouldRetry(err error) bool {
   190  	return false
   191  }
   192  
   193  // Put is specified in the StorageWriter interface.
   194  func (stor *maasStorage) Put(name string, r io.Reader, length int64) error {
   195  	name = stor.prefixWithPrivateNamespace(name)
   196  	data, err := ioutil.ReadAll(io.LimitReader(r, length))
   197  	if err != nil {
   198  		return err
   199  	}
   200  	params := url.Values{"filename": {name}}
   201  	files := map[string][]byte{"file": data}
   202  	snapshot := stor.getSnapshot()
   203  	_, err = snapshot.maasClientUnlocked.CallPostFiles("add", params, files)
   204  	return err
   205  }
   206  
   207  // Remove is specified in the StorageWriter interface.
   208  func (stor *maasStorage) Remove(name string) error {
   209  	name = stor.prefixWithPrivateNamespace(name)
   210  	// The only thing that can go wrong here, really, is that the file
   211  	// does not exist.  But deletion is idempotent: deleting a file that
   212  	// is no longer there anyway is success, not failure.
   213  	stor.getSnapshot().maasClientUnlocked.GetSubObject(name).Delete()
   214  	return nil
   215  }
   216  
   217  // RemoveAll is specified in the StorageWriter interface.
   218  func (stor *maasStorage) RemoveAll() error {
   219  	names, err := storage.List(stor, "")
   220  	if err != nil {
   221  		return err
   222  	}
   223  	// Remove all the objects in parallel so that we incur fewer round-trips.
   224  	// If we're in danger of having hundreds of objects,
   225  	// we'll want to change this to limit the number
   226  	// of concurrent operations.
   227  	var wg sync.WaitGroup
   228  	wg.Add(len(names))
   229  	errc := make(chan error, len(names))
   230  	for _, name := range names {
   231  		name := name
   232  		go func() {
   233  			defer wg.Done()
   234  			if err := stor.Remove(name); err != nil {
   235  				errc <- err
   236  			}
   237  		}()
   238  	}
   239  	wg.Wait()
   240  	select {
   241  	case err := <-errc:
   242  		return fmt.Errorf("cannot delete all provider state: %v", err)
   243  	default:
   244  	}
   245  	return nil
   246  }