github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/cmd/admin-handlers-pools.go (about) 1 // Copyright (c) 2015-2024 MinIO, Inc. 2 // 3 // This file is part of MinIO Object Storage stack 4 // 5 // This program is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU Affero General Public License as published by 7 // the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // This program is distributed in the hope that it will be useful 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU Affero General Public License for more details. 14 // 15 // You should have received a copy of the GNU Affero General Public License 16 // along with this program. If not, see <http://www.gnu.org/licenses/>. 17 18 package cmd 19 20 import ( 21 "context" 22 "encoding/json" 23 "errors" 24 "fmt" 25 "net/http" 26 "strconv" 27 "strings" 28 29 "github.com/minio/minio/internal/logger" 30 "github.com/minio/mux" 31 "github.com/minio/pkg/v2/env" 32 "github.com/minio/pkg/v2/policy" 33 ) 34 35 var ( 36 errRebalanceDecommissionAlreadyRunning = errors.New("Rebalance cannot be started, decommission is already in progress") 37 errDecommissionRebalanceAlreadyRunning = errors.New("Decommission cannot be started, rebalance is already in progress") 38 ) 39 40 func (a adminAPIHandlers) StartDecommission(w http.ResponseWriter, r *http.Request) { 41 ctx := r.Context() 42 43 objectAPI, _ := validateAdminReq(ctx, w, r, policy.DecommissionAdminAction) 44 if objectAPI == nil { 45 return 46 } 47 48 // Legacy args style such as non-ellipses style is not supported with this API. 49 if globalEndpoints.Legacy() { 50 writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL) 51 return 52 } 53 54 z, ok := objectAPI.(*erasureServerPools) 55 if !ok || len(z.serverPools) == 1 { 56 writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL) 57 return 58 } 59 60 if z.IsDecommissionRunning() { 61 writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errDecommissionAlreadyRunning), r.URL) 62 return 63 } 64 65 if z.IsRebalanceStarted() { 66 writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminRebalanceAlreadyStarted), r.URL) 67 return 68 } 69 70 vars := mux.Vars(r) 71 v := vars["pool"] 72 byID := vars["by-id"] == "true" 73 74 pools := strings.Split(v, ",") 75 poolIndices := make([]int, 0, len(pools)) 76 77 for _, pool := range pools { 78 var idx int 79 if byID { 80 var err error 81 idx, err = strconv.Atoi(pool) 82 if err != nil { 83 // We didn't find any matching pools, invalid input 84 writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errInvalidArgument), r.URL) 85 return 86 } 87 } else { 88 idx = globalEndpoints.GetPoolIdx(pool) 89 if idx == -1 { 90 // We didn't find any matching pools, invalid input 91 writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errInvalidArgument), r.URL) 92 return 93 } 94 } 95 var pool *erasureSets 96 for pidx := range z.serverPools { 97 if pidx == idx { 98 pool = z.serverPools[idx] 99 break 100 } 101 } 102 if pool == nil { 103 // We didn't find any matching pools, invalid input 104 writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errInvalidArgument), r.URL) 105 return 106 } 107 108 poolIndices = append(poolIndices, idx) 109 } 110 111 if len(poolIndices) == 0 || !proxyDecommissionRequest(ctx, globalEndpoints[poolIndices[0]].Endpoints[0], w, r) { 112 if err := z.Decommission(r.Context(), poolIndices...); err != nil { 113 writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) 114 return 115 } 116 } 117 } 118 119 func (a adminAPIHandlers) CancelDecommission(w http.ResponseWriter, r *http.Request) { 120 ctx := r.Context() 121 122 objectAPI, _ := validateAdminReq(ctx, w, r, policy.DecommissionAdminAction) 123 if objectAPI == nil { 124 return 125 } 126 127 // Legacy args style such as non-ellipses style is not supported with this API. 128 if globalEndpoints.Legacy() { 129 writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL) 130 return 131 } 132 133 pools, ok := objectAPI.(*erasureServerPools) 134 if !ok { 135 writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL) 136 return 137 } 138 139 vars := mux.Vars(r) 140 v := vars["pool"] 141 byID := vars["by-id"] == "true" 142 idx := -1 143 144 if byID { 145 if i, err := strconv.Atoi(v); err == nil && i >= 0 && i < len(globalEndpoints) { 146 idx = i 147 } 148 } else { 149 idx = globalEndpoints.GetPoolIdx(v) 150 } 151 152 if idx == -1 { 153 // We didn't find any matching pools, invalid input 154 writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errInvalidArgument), r.URL) 155 return 156 } 157 158 if !proxyDecommissionRequest(ctx, globalEndpoints[idx].Endpoints[0], w, r) { 159 if err := pools.DecommissionCancel(ctx, idx); err != nil { 160 writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) 161 return 162 } 163 } 164 } 165 166 func (a adminAPIHandlers) StatusPool(w http.ResponseWriter, r *http.Request) { 167 ctx := r.Context() 168 169 objectAPI, _ := validateAdminReq(ctx, w, r, policy.ServerInfoAdminAction, policy.DecommissionAdminAction) 170 if objectAPI == nil { 171 return 172 } 173 174 // Legacy args style such as non-ellipses style is not supported with this API. 175 if globalEndpoints.Legacy() { 176 writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL) 177 return 178 } 179 180 pools, ok := objectAPI.(*erasureServerPools) 181 if !ok { 182 writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL) 183 return 184 } 185 186 vars := mux.Vars(r) 187 v := vars["pool"] 188 byID := vars["by-id"] == "true" 189 idx := -1 190 191 if byID { 192 if i, err := strconv.Atoi(v); err == nil && i >= 0 && i < len(globalEndpoints) { 193 idx = i 194 } 195 } else { 196 idx = globalEndpoints.GetPoolIdx(v) 197 } 198 199 if idx == -1 { 200 apiErr := toAdminAPIErr(ctx, errInvalidArgument) 201 apiErr.Description = fmt.Sprintf("specified pool '%s' not found, please specify a valid pool", v) 202 // We didn't find any matching pools, invalid input 203 writeErrorResponseJSON(ctx, w, apiErr, r.URL) 204 return 205 } 206 207 status, err := pools.Status(r.Context(), idx) 208 if err != nil { 209 writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) 210 return 211 } 212 213 logger.LogIf(r.Context(), json.NewEncoder(w).Encode(&status)) 214 } 215 216 func (a adminAPIHandlers) ListPools(w http.ResponseWriter, r *http.Request) { 217 ctx := r.Context() 218 219 objectAPI, _ := validateAdminReq(ctx, w, r, policy.ServerInfoAdminAction, policy.DecommissionAdminAction) 220 if objectAPI == nil { 221 return 222 } 223 224 // Legacy args style such as non-ellipses style is not supported with this API. 225 if globalEndpoints.Legacy() { 226 writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL) 227 return 228 } 229 230 pools, ok := objectAPI.(*erasureServerPools) 231 if !ok { 232 writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL) 233 return 234 } 235 236 poolsStatus := make([]PoolStatus, len(globalEndpoints)) 237 for idx := range globalEndpoints { 238 status, err := pools.Status(r.Context(), idx) 239 if err != nil { 240 writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) 241 return 242 } 243 poolsStatus[idx] = status 244 } 245 246 logger.LogIf(r.Context(), json.NewEncoder(w).Encode(poolsStatus)) 247 } 248 249 func (a adminAPIHandlers) RebalanceStart(w http.ResponseWriter, r *http.Request) { 250 ctx := r.Context() 251 252 objectAPI, _ := validateAdminReq(ctx, w, r, policy.RebalanceAdminAction) 253 if objectAPI == nil { 254 return 255 } 256 257 // NB rebalance-start admin API is always coordinated from first pool's 258 // first node. The following is required to serialize (the effects of) 259 // concurrent rebalance-start commands. 260 if ep := globalEndpoints[0].Endpoints[0]; !ep.IsLocal { 261 for nodeIdx, proxyEp := range globalProxyEndpoints { 262 if proxyEp.Endpoint.Host == ep.Host { 263 if proxyRequestByNodeIndex(ctx, w, r, nodeIdx) { 264 return 265 } 266 } 267 } 268 } 269 270 pools, ok := objectAPI.(*erasureServerPools) 271 if !ok || len(pools.serverPools) == 1 { 272 writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL) 273 return 274 } 275 276 if pools.IsDecommissionRunning() { 277 writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errRebalanceDecommissionAlreadyRunning), r.URL) 278 return 279 } 280 281 if pools.IsRebalanceStarted() { 282 writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminRebalanceAlreadyStarted), r.URL) 283 return 284 } 285 286 bucketInfos, err := objectAPI.ListBuckets(ctx, BucketOptions{}) 287 if err != nil { 288 writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL) 289 return 290 } 291 292 buckets := make([]string, 0, len(bucketInfos)) 293 for _, bInfo := range bucketInfos { 294 buckets = append(buckets, bInfo.Name) 295 } 296 297 var id string 298 if id, err = pools.initRebalanceMeta(ctx, buckets); err != nil { 299 writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL) 300 return 301 } 302 303 // Rebalance routine is run on the first node of any pool participating in rebalance. 304 pools.StartRebalance() 305 306 b, err := json.Marshal(struct { 307 ID string `json:"id"` 308 }{ID: id}) 309 if err != nil { 310 writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL) 311 return 312 } 313 314 writeSuccessResponseJSON(w, b) 315 // Notify peers to load rebalance.bin and start rebalance routine if they happen to be 316 // participating pool's leader node 317 globalNotificationSys.LoadRebalanceMeta(ctx, true) 318 } 319 320 func (a adminAPIHandlers) RebalanceStatus(w http.ResponseWriter, r *http.Request) { 321 ctx := r.Context() 322 323 objectAPI, _ := validateAdminReq(ctx, w, r, policy.RebalanceAdminAction) 324 if objectAPI == nil { 325 return 326 } 327 328 // Proxy rebalance-status to first pool first node, so that users see a 329 // consistent view of rebalance progress even though different rebalancing 330 // pools may temporarily have out of date info on the others. 331 if ep := globalEndpoints[0].Endpoints[0]; !ep.IsLocal { 332 for nodeIdx, proxyEp := range globalProxyEndpoints { 333 if proxyEp.Endpoint.Host == ep.Host { 334 if proxyRequestByNodeIndex(ctx, w, r, nodeIdx) { 335 return 336 } 337 } 338 } 339 } 340 341 pools, ok := objectAPI.(*erasureServerPools) 342 if !ok { 343 writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL) 344 return 345 } 346 347 rs, err := rebalanceStatus(ctx, pools) 348 if err != nil { 349 if errors.Is(err, errRebalanceNotStarted) || errors.Is(err, errConfigNotFound) { 350 writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminRebalanceNotStarted), r.URL) 351 return 352 } 353 logger.LogIf(ctx, fmt.Errorf("failed to fetch rebalance status: %w", err)) 354 writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) 355 return 356 } 357 logger.LogIf(r.Context(), json.NewEncoder(w).Encode(rs)) 358 } 359 360 func (a adminAPIHandlers) RebalanceStop(w http.ResponseWriter, r *http.Request) { 361 ctx := r.Context() 362 363 objectAPI, _ := validateAdminReq(ctx, w, r, policy.RebalanceAdminAction) 364 if objectAPI == nil { 365 return 366 } 367 368 pools, ok := objectAPI.(*erasureServerPools) 369 if !ok { 370 writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL) 371 return 372 } 373 374 // Cancel any ongoing rebalance operation 375 globalNotificationSys.StopRebalance(r.Context()) 376 writeSuccessResponseHeadersOnly(w) 377 logger.LogIf(ctx, pools.saveRebalanceStats(GlobalContext, 0, rebalSaveStoppedAt)) 378 } 379 380 func proxyDecommissionRequest(ctx context.Context, defaultEndPoint Endpoint, w http.ResponseWriter, r *http.Request) (proxy bool) { 381 host := env.Get("_MINIO_DECOM_ENDPOINT_HOST", defaultEndPoint.Host) 382 if host == "" { 383 return 384 } 385 for nodeIdx, proxyEp := range globalProxyEndpoints { 386 if proxyEp.Endpoint.Host == host && !proxyEp.IsLocal { 387 if proxyRequestByNodeIndex(ctx, w, r, nodeIdx) { 388 return true 389 } 390 } 391 } 392 return 393 }