github.com/NVIDIA/aistore@v1.3.23-0.20240517131212-7df6609be51d/ais/prxbck.go (about) 1 // Package ais provides core functionality for the AIStore object storage. 2 /* 3 * Copyright (c) 2018-2024, NVIDIA CORPORATION. All rights reserved. 4 */ 5 package ais 6 7 import ( 8 "errors" 9 "net/http" 10 "net/url" 11 "sync" 12 13 "github.com/NVIDIA/aistore/api/apc" 14 "github.com/NVIDIA/aistore/cmn" 15 "github.com/NVIDIA/aistore/cmn/cos" 16 "github.com/NVIDIA/aistore/cmn/debug" 17 "github.com/NVIDIA/aistore/cmn/nlog" 18 "github.com/NVIDIA/aistore/core/meta" 19 "github.com/NVIDIA/aistore/xact" 20 ) 21 22 type bctx struct { 23 w http.ResponseWriter 24 r *http.Request 25 26 p *proxy 27 28 bck *meta.Bck 29 msg *apc.ActMsg 30 31 // URL query: the conventional/slow and 32 // the fast alternative tailored exclusively for the datapath 33 query url.Values 34 dpq *dpq 35 36 origURLBck string 37 38 reqBody []byte // request body of original request 39 perms apc.AccessAttrs // apc.AceGET, apc.AcePATCH etc. 40 41 // 5 user or caller-provided control flags followed by 42 // 3 result flags 43 skipBackend bool // initialize bucket via `bck.InitNoBackend` 44 createAIS bool // create ais bucket on the fly 45 dontAddRemote bool // do not create (ie., add -> BMD) remote bucket on the fly 46 dontHeadRemote bool // do not HEAD remote bucket (to find out whether it exists and/or get properties) 47 tryHeadRemote bool // when listing objects anonymously (via ListObjsMsg.Flags LsTryHeadRemote) 48 isPresent bool // the bucket is confirmed to be present (in the cluster's BMD) 49 exists bool // remote bucket is confirmed to exist 50 modified bool // bucket-defining control structure got modified 51 } 52 53 //////////////// 54 // ibargsPool // 55 //////////////// 56 57 var ( 58 ibargsPool sync.Pool 59 ib0 bctx 60 ) 61 62 func allocBctx() (a *bctx) { 63 if v := ibargsPool.Get(); v != nil { 64 a = v.(*bctx) 65 return 66 } 67 return &bctx{} 68 } 69 70 func freeBctx(a *bctx) { 71 *a = ib0 72 ibargsPool.Put(a) 73 } 74 75 // 76 // lookup and add buckets on the fly 77 // 78 79 func (p *proxy) a2u(aliasOrUUID string) string { 80 p.remais.mu.RLock() 81 for _, remais := range p.remais.A { 82 if aliasOrUUID == remais.Alias || aliasOrUUID == remais.UUID { 83 p.remais.mu.RUnlock() 84 return remais.UUID 85 } 86 } 87 l := len(p.remais.old) 88 for i := l - 1; i >= 0; i-- { 89 remais := p.remais.old[i] 90 if aliasOrUUID == remais.Alias || aliasOrUUID == remais.UUID { 91 p.remais.mu.RUnlock() 92 return remais.UUID 93 } 94 } 95 p.remais.mu.RUnlock() 96 return aliasOrUUID 97 } 98 99 // initialize bucket and check access permissions 100 func (bctx *bctx) init() (ecode int, err error) { 101 debug.Assert(bctx.bck != nil) 102 103 bck := bctx.bck 104 105 // remote ais aliasing 106 if bck.IsRemoteAIS() { 107 if uuid := bctx.p.a2u(bck.Ns.UUID); uuid != bck.Ns.UUID { 108 bctx.modified = true 109 // care of targets 110 query := bctx.query 111 if query == nil { 112 query = bctx.r.URL.Query() 113 } 114 bck.Ns.UUID = uuid 115 query.Set(apc.QparamNamespace, bck.Ns.Uname()) 116 bctx.r.URL.RawQuery = query.Encode() 117 } 118 } 119 120 if err = bctx.accessSupported(); err != nil { 121 ecode = http.StatusMethodNotAllowed 122 return 123 } 124 if bctx.skipBackend { 125 err = bck.InitNoBackend(bctx.p.owner.bmd) 126 } else { 127 err = bck.Init(bctx.p.owner.bmd) 128 } 129 if err != nil { 130 ecode = http.StatusBadRequest 131 if cmn.IsErrBucketNought(err) { 132 ecode = http.StatusNotFound 133 } 134 return 135 } 136 137 bctx.isPresent = true 138 139 // if permissions are not explicitly specified check the default (msg.Action => permissions) 140 if bctx.perms == 0 && bctx.msg != nil { 141 dtor, ok := xact.Table[bctx.msg.Action] 142 if !ok || dtor.Access == 0 { 143 return 144 } 145 bctx.perms = dtor.Access 146 } 147 ecode, err = bctx.accessAllowed(bck) 148 return 149 } 150 151 // returns true when operation requires the 'perm' type access 152 func (bctx *bctx) _perm(perm apc.AccessAttrs) bool { return (bctx.perms & perm) == perm } 153 154 // (compare w/ accessAllowed) 155 func (bctx *bctx) accessSupported() error { 156 if !bctx.bck.IsRemote() { 157 return nil 158 } 159 160 var op string 161 if bctx._perm(apc.AceMoveBucket) { 162 op = "rename/move remote bucket" 163 goto rerr 164 } 165 // HTTP buckets are not writeable 166 if bctx.bck.IsHTTP() && bctx._perm(apc.AcePUT) { 167 op = "write to HTTP bucket" 168 goto rerr 169 } 170 // Cloud bucket: destroy op. not allowed, and not supported yet 171 // (have no separate perm for eviction, that's why an extra check) 172 if rmb := bctx.bck.IsCloud() && bctx._perm(apc.AceDestroyBucket) && bctx.msg.Action == apc.ActDestroyBck; !rmb { 173 return nil 174 } 175 op = "destroy cloud bucket" 176 rerr: 177 return cmn.NewErrUnsupp(op, bctx.bck.Cname("")) 178 } 179 180 // (compare w/ accessSupported) 181 func (bctx *bctx) accessAllowed(bck *meta.Bck) (ecode int, err error) { 182 err = bctx.p.access(bctx.r.Header, bck, bctx.perms) 183 ecode = aceErrToCode(err) 184 return ecode, err 185 } 186 187 // initAndTry initializes the bucket (proxy-only, as the filename implies). 188 // The method _may_ try to add it to the BMD if the bucket doesn't exist. 189 // NOTE: 190 // - on error it calls `p.writeErr` and friends, so make sure _not_ to do the same in the caller 191 // - for remais buckets: user-provided alias(***) 192 func (bctx *bctx) initAndTry() (bck *meta.Bck, err error) { 193 var ecode int 194 195 // 1. init bucket 196 bck = bctx.bck 197 if ecode, err = bctx.init(); err == nil { 198 return 199 } 200 if ecode != http.StatusNotFound { 201 bctx.p.writeErr(bctx.w, bctx.r, err, ecode) 202 return 203 } 204 // 2. handle two specific errors 205 switch { 206 case cmn.IsErrBckNotFound(err): 207 debug.Assert(bck.IsAIS()) 208 if !bctx.createAIS { 209 if bctx.perms == apc.AceBckHEAD { 210 bctx.p.writeErr(bctx.w, bctx.r, err, ecode, Silent) 211 } else { 212 bctx.p.writeErr(bctx.w, bctx.r, err, ecode) 213 } 214 return 215 } 216 case cmn.IsErrRemoteBckNotFound(err): 217 debug.Assert(bck.IsRemote()) 218 // when remote-bucket lookup is not permitted 219 if bctx.dontHeadRemote { 220 bctx.p.writeErr(bctx.w, bctx.r, err, ecode, Silent) 221 return 222 } 223 default: 224 debug.Assertf(false, "%q: unexpected %v(%d)", bctx.bck, err, ecode) 225 bctx.p.writeErr(bctx.w, bctx.r, err, ecode) 226 return 227 } 228 229 // 3. create ais bucket _or_ lookup and, *if* confirmed, add remote bucket to the BMD 230 // (see also: "on the fly") 231 return bctx.try() 232 } 233 234 func (bctx *bctx) try() (bck *meta.Bck, err error) { 235 bck, ecode, err := bctx._try() 236 switch { 237 case err == nil || err == errForwarded: 238 return bck, err 239 case cmn.IsErrBucketAlreadyExists(err): 240 // e.g., when (re)setting backend two times in a row 241 nlog.Infoln(bctx.p.String()+":", err, " - nothing to do") 242 return bck, nil 243 default: 244 if bctx.perms == apc.AceBckHEAD { 245 bctx.p.writeErr(bctx.w, bctx.r, err, ecode, Silent) 246 } else { 247 bctx.p.writeErr(bctx.w, bctx.r, err, ecode) 248 } 249 return bck, err 250 } 251 } 252 253 // 254 // methods that are internal to this source 255 // 256 257 func (bctx *bctx) _try() (bck *meta.Bck, ecode int, err error) { 258 if err = bctx.bck.Validate(); err != nil { 259 ecode = http.StatusBadRequest 260 return 261 } 262 263 if bctx.p.forwardCP(bctx.w, bctx.r, bctx.msg, "add-bucket", bctx.reqBody) { 264 err = errForwarded 265 return 266 } 267 268 // am primary from this point on 269 bck = bctx.bck 270 var ( 271 action = apc.ActCreateBck 272 remoteHdr http.Header 273 ) 274 if backend := bck.Backend(); backend != nil { 275 bck = backend 276 } 277 if bck.IsAIS() { 278 if err = bctx.p.access(bctx.r.Header, nil /*bck*/, apc.AceCreateBucket); err != nil { 279 ecode = aceErrToCode(err) 280 return 281 } 282 nlog.Warningf("%s: %q doesn't exist, proceeding to create", bctx.p, bctx.bck) 283 goto creadd 284 } 285 action = apc.ActAddRemoteBck // only if requested via bctx 286 287 // lookup remote 288 if remoteHdr, ecode, err = bctx.lookup(bck); err != nil { 289 bck = nil 290 return 291 } 292 293 // orig-url for the ht:// bucket 294 if bck.IsHTTP() { 295 if bctx.origURLBck != "" { 296 remoteHdr.Set(apc.HdrOrigURLBck, bctx.origURLBck) 297 } else { 298 var ( 299 hbo *cmn.HTTPBckObj 300 origURL = bctx.getOrigURL() 301 ) 302 if origURL == "" { 303 err = cmn.NewErrFailedTo(bctx.p, "initialize", bctx.bck, errors.New("missing HTTP URL")) 304 return 305 } 306 if hbo, err = cmn.NewHTTPObjPath(origURL); err != nil { 307 return 308 } 309 remoteHdr.Set(apc.HdrOrigURLBck, hbo.OrigURLBck) 310 } 311 } 312 313 // when explicitly asked _not to_ 314 bctx.exists = true 315 if bctx.dontAddRemote { 316 if bck.IsRemoteAIS() { 317 bck.Props, err = remoteBckProps(bckPropsArgs{bck: bck, hdr: remoteHdr}) 318 } else { 319 // Background (#18995): 320 // 321 // The bucket is not in the BMD - has no local representation. The best we could do 322 // is return remote metadata as is. But there's no control structure for that 323 // other than (AIS-only) `BucketProps`. 324 // Therefore: return the result of merging cluster defaults with the remote header 325 // resulting from the backend.Head(bucket) call and containing actual 326 // values (e.g. versioning). 327 // The returned bucket props will have its BID == 0 (zero), which also means: 328 // this bucket is not initialized and/or not present in BMD. 329 330 bck.Props = defaultBckProps(bckPropsArgs{bck: bck, hdr: remoteHdr}) 331 } 332 return 333 } 334 335 // add/create 336 creadd: 337 if err = bctx.p.createBucket(&apc.ActMsg{Action: action}, bck, remoteHdr); err != nil { 338 ecode = crerrStatus(err) 339 return 340 } 341 // finally, initialize the newly added/created 342 if err = bck.Init(bctx.p.owner.bmd); err != nil { 343 debug.AssertNoErr(err) 344 ecode = http.StatusInternalServerError 345 err = cmn.NewErrFailedTo(bctx.p, "post create-bucket init", bck, err, ecode) 346 } 347 bck = bctx.bck 348 return 349 } 350 351 func (bctx *bctx) getOrigURL() (ourl string) { 352 if bctx.query != nil { 353 debug.Assert(bctx.dpq == nil) 354 ourl = bctx.query.Get(apc.QparamOrigURL) 355 } else { 356 ourl = bctx.dpq.origURL 357 } 358 return 359 } 360 361 func (bctx *bctx) lookup(bck *meta.Bck) (hdr http.Header, code int, err error) { 362 var ( 363 q = url.Values{} 364 retried bool 365 ) 366 if bck.IsHTTP() { 367 origURL := bctx.getOrigURL() 368 q.Set(apc.QparamOrigURL, origURL) 369 } 370 if bctx.tryHeadRemote { 371 q.Set(apc.QparamSilent, "true") 372 } 373 retry: 374 hdr, code, err = bctx.p.headRemoteBck(bck.Bucket(), q) 375 376 if (code == http.StatusUnauthorized || code == http.StatusForbidden) && bctx.tryHeadRemote { 377 if bctx.dontAddRemote { 378 return 379 } 380 // NOTE: assuming OK 381 nlog.Warningf("Proceeding to add remote bucket %s to the BMD after getting err: %v(%d)", bck, err, code) 382 nlog.Warningf("Using all cluster defaults for %s property values", bck) 383 hdr = make(http.Header, 2) 384 hdr.Set(apc.HdrBackendProvider, bck.Provider) 385 hdr.Set(apc.HdrBucketVerEnabled, "false") 386 err = nil 387 return 388 } 389 // NOTE: retrying once (via random target) 390 if err != nil && !retried && cos.IsErrClientURLTimeout(err) { 391 nlog.Warningf("%s: HEAD(%s) timeout %q - retrying...", bctx.p, bck, errors.Unwrap(err)) 392 retried = true 393 goto retry 394 } 395 return 396 }