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