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 }