github.com/altoros/juju-vmware@v0.0.0-20150312064031-f19ae857ccca/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  	"github.com/juju/errors"
    18  	"github.com/juju/utils"
    19  	"launchpad.net/gomaasapi"
    20  
    21  	"github.com/juju/juju/environs/storage"
    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,
    72  // including 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.IsNotFound if the file did not
    76  // exist.
    77  //
    78  // The function takes out a lock on the storage object.
    79  func (stor *maasStorage) retrieveFileObject(name string) (gomaasapi.MAASObject, error) {
    80  	obj, err := stor.addressFileObject(name).Get()
    81  	if err != nil {
    82  		noObj := gomaasapi.MAASObject{}
    83  		serverErr, ok := err.(gomaasapi.ServerError)
    84  		if ok && serverErr.StatusCode == 404 {
    85  			return noObj, errors.NotFoundf("file '%s' not found", name)
    86  		}
    87  		msg := fmt.Errorf("could not access file '%s': %v", name, err)
    88  		return noObj, msg
    89  	}
    90  	return obj, nil
    91  }
    92  
    93  // All filenames need to be namespaced so they are private to this environment.
    94  // This prevents different environments from interfering with each other.
    95  // We're using the agent name UUID here.
    96  func (stor *maasStorage) prefixWithPrivateNamespace(name string) string {
    97  	env := stor.getSnapshot().environUnlocked
    98  	prefix := env.ecfg().maasAgentName()
    99  	if prefix != "" {
   100  		return prefix + "-" + name
   101  	}
   102  	return name
   103  }
   104  
   105  // Get is specified in the StorageReader interface.
   106  func (stor *maasStorage) Get(name string) (io.ReadCloser, error) {
   107  	name = stor.prefixWithPrivateNamespace(name)
   108  	fileObj, err := stor.retrieveFileObject(name)
   109  	if err != nil {
   110  		return nil, err
   111  	}
   112  	data, err := fileObj.GetField("content")
   113  	if err != nil {
   114  		return nil, fmt.Errorf("could not extract file content for %s: %v", name, err)
   115  	}
   116  	buf, err := base64.StdEncoding.DecodeString(data)
   117  	if err != nil {
   118  		return nil, fmt.Errorf("bad data in file '%s': %v", name, err)
   119  	}
   120  	return ioutil.NopCloser(bytes.NewReader(buf)), nil
   121  }
   122  
   123  // extractFilenames returns the filenames from a "list" operation on the
   124  // MAAS API, sorted by name.
   125  func (stor *maasStorage) extractFilenames(listResult gomaasapi.JSONObject) ([]string, error) {
   126  	privatePrefix := stor.prefixWithPrivateNamespace("")
   127  	list, err := listResult.GetArray()
   128  	if err != nil {
   129  		return nil, err
   130  	}
   131  	result := make([]string, len(list))
   132  	for index, entry := range list {
   133  		file, err := entry.GetMap()
   134  		if err != nil {
   135  			return nil, err
   136  		}
   137  		filename, err := file["filename"].GetString()
   138  		if err != nil {
   139  			return nil, err
   140  		}
   141  		// When listing files we need to return them without our special prefix.
   142  		result[index] = strings.TrimPrefix(filename, privatePrefix)
   143  	}
   144  	sort.Strings(result)
   145  	return result, nil
   146  }
   147  
   148  // List is specified in the StorageReader interface.
   149  func (stor *maasStorage) List(prefix string) ([]string, error) {
   150  	prefix = stor.prefixWithPrivateNamespace(prefix)
   151  	params := make(url.Values)
   152  	params.Add("prefix", prefix)
   153  	snapshot := stor.getSnapshot()
   154  	obj, err := snapshot.maasClientUnlocked.CallGet("list", params)
   155  	if err != nil {
   156  		return nil, err
   157  	}
   158  	return snapshot.extractFilenames(obj)
   159  }
   160  
   161  // URL is specified in the StorageReader interface.
   162  func (stor *maasStorage) URL(name string) (string, error) {
   163  	name = stor.prefixWithPrivateNamespace(name)
   164  	fileObj, err := stor.retrieveFileObject(name)
   165  	if err != nil {
   166  		return "", err
   167  	}
   168  	uri, err := fileObj.GetField("anon_resource_uri")
   169  	if err != nil {
   170  		msg := fmt.Errorf("could not get file's download URL (may be an outdated MAAS): %s", err)
   171  		return "", msg
   172  	}
   173  
   174  	partialURL, err := url.Parse(uri)
   175  	if err != nil {
   176  		return "", err
   177  	}
   178  	fullURL := fileObj.URL().ResolveReference(partialURL)
   179  	return fullURL.String(), nil
   180  }
   181  
   182  // DefaultConsistencyStrategy is specified in the StorageReader interface.
   183  func (stor *maasStorage) DefaultConsistencyStrategy() utils.AttemptStrategy {
   184  	// This storage backend has immediate consistency, so there's no
   185  	// need to wait.  One attempt should do.
   186  	return utils.AttemptStrategy{}
   187  }
   188  
   189  // ShouldRetry is specified in the StorageReader interface.
   190  func (stor *maasStorage) ShouldRetry(err error) bool {
   191  	return false
   192  }
   193  
   194  // Put is specified in the StorageWriter interface.
   195  func (stor *maasStorage) Put(name string, r io.Reader, length int64) error {
   196  	name = stor.prefixWithPrivateNamespace(name)
   197  	data, err := ioutil.ReadAll(io.LimitReader(r, length))
   198  	if err != nil {
   199  		return err
   200  	}
   201  	params := url.Values{"filename": {name}}
   202  	files := map[string][]byte{"file": data}
   203  	snapshot := stor.getSnapshot()
   204  	_, err = snapshot.maasClientUnlocked.CallPostFiles("add", params, files)
   205  	return err
   206  }
   207  
   208  // Remove is specified in the StorageWriter interface.
   209  func (stor *maasStorage) Remove(name string) error {
   210  	name = stor.prefixWithPrivateNamespace(name)
   211  	// The only thing that can go wrong here, really, is that the file
   212  	// does not exist.  But deletion is idempotent: deleting a file that
   213  	// is no longer there anyway is success, not failure.
   214  	stor.getSnapshot().maasClientUnlocked.GetSubObject(name).Delete()
   215  	return nil
   216  }
   217  
   218  // RemoveAll is specified in the StorageWriter interface.
   219  func (stor *maasStorage) RemoveAll() error {
   220  	names, err := storage.List(stor, "")
   221  	if err != nil {
   222  		return err
   223  	}
   224  	// Remove all the objects in parallel so that we incur fewer round-trips.
   225  	// If we're in danger of having hundreds of objects,
   226  	// we'll want to change this to limit the number
   227  	// of concurrent operations.
   228  	var wg sync.WaitGroup
   229  	wg.Add(len(names))
   230  	errc := make(chan error, len(names))
   231  	for _, name := range names {
   232  		name := name
   233  		go func() {
   234  			defer wg.Done()
   235  			if err := stor.Remove(name); err != nil {
   236  				errc <- err
   237  			}
   238  		}()
   239  	}
   240  	wg.Wait()
   241  	select {
   242  	case err := <-errc:
   243  		return fmt.Errorf("cannot delete all provider state: %v", err)
   244  	default:
   245  	}
   246  	return nil
   247  }