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 }