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  }