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 }