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