github.com/NVIDIA/aistore@v1.3.23-0.20240517131212-7df6609be51d/tools/client.go (about)

     1  // Package tools provides common tools and utilities for all unit and integration tests
     2  /*
     3   * Copyright (c) 2018-2024, NVIDIA CORPORATION. All rights reserved.
     4   */
     5  package tools
     6  
     7  import (
     8  	"errors"
     9  	"fmt"
    10  	"math/rand"
    11  	"net/http"
    12  	"path"
    13  	"strconv"
    14  	"sync"
    15  	"testing"
    16  	"time"
    17  
    18  	"github.com/NVIDIA/aistore/api"
    19  	"github.com/NVIDIA/aistore/api/apc"
    20  	"github.com/NVIDIA/aistore/cmn"
    21  	"github.com/NVIDIA/aistore/cmn/atomic"
    22  	"github.com/NVIDIA/aistore/cmn/cos"
    23  	"github.com/NVIDIA/aistore/core/meta"
    24  	"github.com/NVIDIA/aistore/stats"
    25  	"github.com/NVIDIA/aistore/tools/readers"
    26  	"github.com/NVIDIA/aistore/tools/tassert"
    27  	"github.com/NVIDIA/aistore/tools/tlog"
    28  	"github.com/NVIDIA/aistore/tools/trand"
    29  	"github.com/NVIDIA/aistore/xact"
    30  	"golang.org/x/sync/errgroup"
    31  )
    32  
    33  const (
    34  	// This value is holds the input of 'proxyURLFlag' from init_tests.go.
    35  	// It is used in BaseAPIParams to determine if the cluster is running
    36  	// on a
    37  	// 	1. local instance (no docker) 	- works
    38  	//	2. local docker instance		- works
    39  	// 	3. AWS-deployed cluster 		- not tested (but runs mainly with Ansible)
    40  	MockDaemonID = "MOCK"
    41  )
    42  
    43  // times and timeouts
    44  const (
    45  	WaitClusterStartup    = 20 * time.Second
    46  	RebalanceStartTimeout = 10 * time.Second
    47  	MaxCplaneTimeout      = 10 * time.Second
    48  
    49  	CopyBucketTimeout     = 3 * time.Minute
    50  	MultiProxyTestTimeout = 3 * time.Minute
    51  
    52  	DsortFinishTimeout   = 6 * time.Minute
    53  	RebalanceTimeout     = 2 * time.Minute
    54  	EvictPrefetchTimeout = 2 * time.Minute
    55  	BucketCleanupTimeout = time.Minute
    56  
    57  	xactPollSleep     = time.Second
    58  	controlPlaneSleep = 2 * time.Second
    59  )
    60  
    61  type PutObjectsArgs struct {
    62  	ProxyURL  string
    63  	Bck       cmn.Bck
    64  	ObjPath   string
    65  	CksumType string
    66  	ObjSize   uint64
    67  	ObjCnt    int
    68  	WorkerCnt int
    69  	FixedSize bool
    70  	Ordered   bool // true - object names make sequence, false - names are random
    71  	IgnoreErr bool
    72  }
    73  
    74  func Del(proxyURL string, bck cmn.Bck, object string, wg *sync.WaitGroup, errCh chan error, silent bool) error {
    75  	if wg != nil {
    76  		defer wg.Done()
    77  	}
    78  	if !silent {
    79  		fmt.Printf("DEL: %s\n", object)
    80  	}
    81  	bp := BaseAPIParams(proxyURL)
    82  	err := api.DeleteObject(bp, bck, object)
    83  	if err != nil && errCh != nil {
    84  		errCh <- err
    85  	}
    86  	return err
    87  }
    88  
    89  func CheckObjIsPresent(proxyURL string, bck cmn.Bck, objName string) bool {
    90  	bp := BaseAPIParams(proxyURL)
    91  	_, err := api.HeadObject(bp, bck, objName, apc.FltPresent, true /*silent*/)
    92  	return err == nil
    93  }
    94  
    95  // Put sends a PUT request to the given URL
    96  func Put(proxyURL string, bck cmn.Bck, objName string, reader readers.Reader, errCh chan error) {
    97  	bp := BaseAPIParams(proxyURL)
    98  	putArgs := api.PutArgs{
    99  		BaseParams: bp,
   100  		Bck:        bck,
   101  		ObjName:    objName,
   102  		Cksum:      reader.Cksum(),
   103  		Reader:     reader,
   104  	}
   105  	_, err := api.PutObject(&putArgs)
   106  	if err == nil {
   107  		return
   108  	}
   109  	if errCh == nil {
   110  		fmt.Printf("Failed to PUT %s: %v (nil error channel)\n", bck.Cname(objName), err)
   111  	} else {
   112  		errCh <- err
   113  	}
   114  }
   115  
   116  // PutObject sends a PUT request to the given URL.
   117  func PutObject(t *testing.T, bck cmn.Bck, objName string, reader readers.Reader) {
   118  	var (
   119  		proxyURL = RandomProxyURL()
   120  		errCh    = make(chan error, 1)
   121  	)
   122  	Put(proxyURL, bck, objName, reader, errCh)
   123  	tassert.SelectErr(t, errCh, "put", true)
   124  }
   125  
   126  // ListObjectNames returns a slice of object names of all objects that match the prefix in a bucket
   127  func ListObjectNames(proxyURL string, bck cmn.Bck, prefix string, objectCountLimit int64, cached bool) ([]string, error) {
   128  	var (
   129  		bp  = BaseAPIParams(proxyURL)
   130  		msg = &apc.LsoMsg{Prefix: prefix}
   131  	)
   132  	if cached {
   133  		msg.Flags = apc.LsObjCached
   134  	}
   135  	data, err := api.ListObjects(bp, bck, msg, api.ListArgs{Limit: objectCountLimit})
   136  	if err != nil {
   137  		return nil, err
   138  	}
   139  
   140  	objs := make([]string, 0, len(data.Entries))
   141  	for _, obj := range data.Entries {
   142  		objs = append(objs, obj.Name)
   143  	}
   144  	return objs, nil
   145  }
   146  
   147  func GetPrimaryURL() string {
   148  	primary, err := GetPrimaryProxy(proxyURLReadOnly)
   149  	if err == nil {
   150  		return primary.URL(cmn.NetPublic)
   151  	}
   152  	fmt.Printf("Warning: GetPrimaryProxy [%v] - retrying once...\n", err)
   153  	if currSmap == nil {
   154  		time.Sleep(time.Second)
   155  		primary, err = GetPrimaryProxy(proxyURLReadOnly)
   156  	} else if proxyURL := currSmap.Primary.URL(cmn.NetPublic); proxyURL != proxyURLReadOnly {
   157  		primary, err = GetPrimaryProxy(proxyURL)
   158  	} else {
   159  		var psi *meta.Snode
   160  		if psi, err = currSmap.GetRandProxy(true /*exclude primary*/); err == nil {
   161  			primary, err = GetPrimaryProxy(psi.URL(cmn.NetPublic))
   162  		}
   163  	}
   164  	if err != nil {
   165  		fmt.Printf("Warning: GetPrimaryProxy [%v] - returning global %q\n", err, proxyURLReadOnly)
   166  		return proxyURLReadOnly
   167  	}
   168  	return primary.URL(cmn.NetPublic)
   169  }
   170  
   171  // GetPrimaryProxy returns the primary proxy
   172  func GetPrimaryProxy(proxyURL string) (*meta.Snode, error) {
   173  	bp := BaseAPIParams(proxyURL)
   174  	smap, err := api.GetClusterMap(bp)
   175  	if err != nil {
   176  		return nil, err
   177  	}
   178  	if currSmap == nil || currSmap.Version < smap.Version {
   179  		currSmap = smap
   180  	}
   181  	return smap.Primary, err
   182  }
   183  
   184  func GetProxyReadiness(proxyURL string) error {
   185  	return api.GetProxyReadiness(BaseAPIParams(proxyURL))
   186  }
   187  
   188  func CreateBucket(tb testing.TB, proxyURL string, bck cmn.Bck, props *cmn.BpropsToSet, cleanup bool) {
   189  	bp := BaseAPIParams(proxyURL)
   190  	err := api.CreateBucket(bp, bck, props)
   191  	tassert.CheckFatal(tb, err)
   192  	if cleanup {
   193  		tb.Cleanup(func() {
   194  			DestroyBucket(tb, proxyURL, bck)
   195  		})
   196  	}
   197  }
   198  
   199  // is usually called to cleanup (via tb.Cleanup)
   200  func DestroyBucket(tb testing.TB, proxyURL string, bck cmn.Bck) {
   201  	bp := BaseAPIParams(proxyURL)
   202  	exists, err := api.QueryBuckets(bp, cmn.QueryBcks(bck), apc.FltExists)
   203  	tassert.CheckFatal(tb, err)
   204  	if exists {
   205  		err = api.DestroyBucket(bp, bck)
   206  		if err == nil {
   207  			return
   208  		}
   209  		herr := cmn.Err2HTTPErr(err)
   210  		if herr == nil || herr.Status != http.StatusNotFound {
   211  			tassert.CheckFatal(tb, err)
   212  		}
   213  	}
   214  }
   215  
   216  func EvictRemoteBucket(tb testing.TB, proxyURL string, bck cmn.Bck) {
   217  	if backend := bck.Backend(); backend != nil {
   218  		bck.Copy(backend)
   219  	}
   220  	err := api.EvictRemoteBucket(BaseAPIParams(proxyURL), bck, false)
   221  	tassert.CheckFatal(tb, err)
   222  }
   223  
   224  func CleanupRemoteBucket(t *testing.T, proxyURL string, bck cmn.Bck, prefix string) {
   225  	if !bck.IsRemote() {
   226  		return
   227  	}
   228  
   229  	toDelete, err := ListObjectNames(proxyURL, bck, prefix, 0, false /*cached*/)
   230  	tassert.CheckFatal(t, err)
   231  	defer EvictRemoteBucket(t, proxyURL, bck)
   232  
   233  	if len(toDelete) == 0 {
   234  		return
   235  	}
   236  
   237  	bp := BaseAPIParams(proxyURL)
   238  	xid, err := api.DeleteMultiObj(bp, bck, toDelete, "" /*template*/)
   239  	tassert.CheckFatal(t, err)
   240  	args := xact.ArgsMsg{ID: xid, Kind: apc.ActDeleteObjects, Timeout: BucketCleanupTimeout}
   241  	_, err = api.WaitForXactionIC(bp, &args)
   242  	tassert.CheckFatal(t, err)
   243  }
   244  
   245  func SetBackendBck(t *testing.T, bp api.BaseParams, srcBck, dstBck cmn.Bck) {
   246  	// find out real provider of the bucket
   247  	p, err := api.HeadBucket(bp, dstBck, false /* don't add to cluster MD */)
   248  	tassert.CheckFatal(t, err)
   249  
   250  	_, err = api.SetBucketProps(bp, srcBck, &cmn.BpropsToSet{
   251  		BackendBck: &cmn.BackendBckToSet{
   252  			Name:     apc.Ptr(dstBck.Name),
   253  			Provider: apc.Ptr(p.Provider),
   254  		},
   255  	})
   256  	tassert.CheckFatal(t, err)
   257  }
   258  
   259  func RmTargetSkipRebWait(t *testing.T, proxyURL string, smap *meta.Smap) (*meta.Smap, *meta.Snode) {
   260  	var (
   261  		removeTarget, _ = smap.GetRandTarget()
   262  		origTgtCnt      = smap.CountActiveTs()
   263  		args            = &apc.ActValRmNode{DaemonID: removeTarget.ID(), SkipRebalance: true}
   264  	)
   265  	_, err := api.StartMaintenance(BaseAPIParams(proxyURL), args)
   266  	tassert.CheckFatal(t, err)
   267  	newSmap, err := WaitForClusterState(
   268  		proxyURL,
   269  		"target is gone",
   270  		smap.Version,
   271  		smap.CountActivePs(),
   272  		origTgtCnt-1,
   273  	)
   274  	tassert.CheckFatal(t, err)
   275  	newTgtCnt := newSmap.CountActiveTs()
   276  	tassert.Fatalf(t, newTgtCnt == origTgtCnt-1,
   277  		"new smap expected to have 1 target less: %d (v%d) vs %d (v%d)", newTgtCnt, origTgtCnt,
   278  		newSmap.Version, smap.Version)
   279  	return newSmap, removeTarget
   280  }
   281  
   282  // Internal API to remove a node from Smap: use it to unregister MOCK targets/proxies.
   283  // Use `JoinCluster` to attach node back.
   284  func RemoveNodeUnsafe(proxyURL, sid string) error {
   285  	return _removeNodeFromSmap(proxyURL, sid, MaxCplaneTimeout)
   286  }
   287  
   288  func WaitForObjectToBeDowloaded(bp api.BaseParams, bck cmn.Bck, objName string, timeout time.Duration) error {
   289  	maxTime := time.Now().Add(timeout)
   290  	for {
   291  		if time.Now().After(maxTime) {
   292  			return fmt.Errorf("timed out (%v) waiting for %s download", timeout, bck.Cname(objName))
   293  		}
   294  		reslist, err := api.ListObjects(bp, bck, &apc.LsoMsg{}, api.ListArgs{})
   295  		if err != nil {
   296  			return err
   297  		}
   298  		for _, obj := range reslist.Entries {
   299  			if obj.Name == objName {
   300  				return nil
   301  			}
   302  		}
   303  		time.Sleep(500 * time.Millisecond)
   304  	}
   305  }
   306  
   307  func EnsureObjectsExist(t *testing.T, params api.BaseParams, bck cmn.Bck, objectsNames ...string) {
   308  	for _, objName := range objectsNames {
   309  		_, err := api.GetObject(params, bck, objName, nil)
   310  		if err != nil {
   311  			t.Errorf("Unexpected GetObject(%s) error: %v.", objName, err)
   312  		}
   313  	}
   314  }
   315  
   316  //nolint:gocritic // need a copy of PutObjectsArgs
   317  func PutRandObjs(args PutObjectsArgs) ([]string, int, error) {
   318  	var (
   319  		errCnt = atomic.NewInt32(0)
   320  		putCnt = atomic.NewInt32(0)
   321  
   322  		workerCnt = 40 // Default worker count.
   323  		group     = &errgroup.Group{}
   324  		objNames  = make([]string, 0, args.ObjCnt)
   325  		bp        = BaseAPIParams(args.ProxyURL)
   326  	)
   327  
   328  	if args.WorkerCnt > 0 {
   329  		workerCnt = args.WorkerCnt
   330  	}
   331  	workerCnt = min(workerCnt, args.ObjCnt)
   332  
   333  	for i := range args.ObjCnt {
   334  		if args.Ordered {
   335  			objNames = append(objNames, path.Join(args.ObjPath, strconv.Itoa(i)))
   336  		} else {
   337  			objNames = append(objNames, path.Join(args.ObjPath, trand.String(16)))
   338  		}
   339  	}
   340  	chunkSize := (len(objNames) + workerCnt - 1) / workerCnt
   341  	for i := 0; i < len(objNames); i += chunkSize {
   342  		group.Go(func(start, end int) func() error {
   343  			return func() error {
   344  				for _, objName := range objNames[start:end] {
   345  					size := args.ObjSize
   346  					if size == 0 { // Size not specified so generate something.
   347  						size = uint64(cos.NowRand().Intn(cos.KiB)+1) * cos.KiB
   348  					} else if !args.FixedSize { // Randomize object size.
   349  						size += uint64(rand.Int63n(cos.KiB))
   350  					}
   351  
   352  					if args.CksumType == "" {
   353  						args.CksumType = cos.ChecksumNone
   354  					}
   355  
   356  					reader, err := readers.NewRand(int64(size), args.CksumType)
   357  					cos.AssertNoErr(err)
   358  
   359  					// We could PUT while creating files, but that makes it
   360  					// begin all the puts immediately (because creating random files is fast
   361  					// compared to the list objects call that getRandomFiles does)
   362  					_, err = api.PutObject(&api.PutArgs{
   363  						BaseParams: bp,
   364  						Bck:        args.Bck,
   365  						ObjName:    objName,
   366  						Cksum:      reader.Cksum(),
   367  						Reader:     reader,
   368  						Size:       size,
   369  						SkipVC:     true,
   370  					})
   371  					putCnt.Inc()
   372  					if err != nil {
   373  						if args.IgnoreErr {
   374  							errCnt.Inc()
   375  							return nil
   376  						}
   377  						return err
   378  					}
   379  				}
   380  				return nil
   381  			}
   382  		}(i, min(i+chunkSize, len(objNames))))
   383  	}
   384  
   385  	err := group.Wait()
   386  	cos.Assert(err != nil || len(objNames) == int(putCnt.Load()))
   387  	return objNames, int(errCnt.Load()), err
   388  }
   389  
   390  // Put an object into a cloud bucket and evict it afterwards - can be used to test cold GET
   391  func PutObjectInRemoteBucketWithoutCachingLocally(t *testing.T, bck cmn.Bck, object string, objContent cos.ReadOpenCloser) {
   392  	bp := BaseAPIParams()
   393  
   394  	_, err := api.PutObject(&api.PutArgs{
   395  		BaseParams: bp,
   396  		Bck:        bck,
   397  		ObjName:    object,
   398  		Reader:     objContent,
   399  	})
   400  	tassert.CheckFatal(t, err)
   401  
   402  	err = api.EvictObject(bp, bck, object)
   403  	tassert.CheckFatal(t, err)
   404  }
   405  
   406  func GetObjectAtime(t *testing.T, bp api.BaseParams, bck cmn.Bck, object, timeFormat string) (time.Time, string) {
   407  	msg := &apc.LsoMsg{Props: apc.GetPropsAtime, TimeFormat: timeFormat, Prefix: object}
   408  	bucketList, err := api.ListObjects(bp, bck, msg, api.ListArgs{})
   409  	tassert.CheckFatal(t, err)
   410  
   411  	for _, entry := range bucketList.Entries {
   412  		if entry.Name == object {
   413  			atime, err := time.Parse(timeFormat, entry.Atime)
   414  			tassert.CheckFatal(t, err)
   415  			return atime, entry.Atime
   416  		}
   417  	}
   418  
   419  	tassert.Fatalf(t, false, "Cannot find %s in bucket %s", object, bck)
   420  	return time.Time{}, ""
   421  }
   422  
   423  // WaitForDsortToFinish waits until all dSorts jobs finished without failure or
   424  // all jobs abort.
   425  func WaitForDsortToFinish(proxyURL, managerUUID string) (allAborted bool, err error) {
   426  	tlog.Logf("waiting for dsort[%s]\n", managerUUID)
   427  
   428  	bp := BaseAPIParams(proxyURL)
   429  	deadline := time.Now().Add(DsortFinishTimeout)
   430  	for time.Now().Before(deadline) {
   431  		all, err := api.MetricsDsort(bp, managerUUID)
   432  		if err != nil {
   433  			return false, err
   434  		}
   435  
   436  		allAborted := true
   437  		allFinished := true
   438  		for _, jmetrics := range all {
   439  			m := jmetrics.Metrics
   440  			allAborted = allAborted && m.Aborted.Load()
   441  			allFinished = allFinished &&
   442  				!m.Aborted.Load() &&
   443  				m.Extraction.Finished &&
   444  				m.Sorting.Finished &&
   445  				m.Creation.Finished
   446  		}
   447  		if allAborted {
   448  			return true, nil
   449  		}
   450  		if allFinished {
   451  			return false, nil
   452  		}
   453  		time.Sleep(500 * time.Millisecond)
   454  	}
   455  	return false, errors.New("deadline exceeded")
   456  }
   457  
   458  func BaseAPIParams(urls ...string) api.BaseParams {
   459  	var u string
   460  	if len(urls) > 0 && urls[0] != "" {
   461  		u = urls[0]
   462  	} else {
   463  		u = RandomProxyURL()
   464  	}
   465  	return api.BaseParams{Client: gctx.Client, URL: u, Token: LoggedUserToken, UA: "tools/test"}
   466  }
   467  
   468  func EvictObjects(t *testing.T, proxyURL string, bck cmn.Bck, objList []string) {
   469  	bp := BaseAPIParams(proxyURL)
   470  	xid, err := api.EvictMultiObj(bp, bck, objList, "" /*template*/)
   471  	if err != nil {
   472  		t.Errorf("Evict bucket %s failed, err = %v", bck, err)
   473  	}
   474  
   475  	args := xact.ArgsMsg{ID: xid, Kind: apc.ActEvictObjects, Timeout: EvictPrefetchTimeout}
   476  	if _, err := api.WaitForXactionIC(bp, &args); err != nil {
   477  		t.Errorf("Wait for xaction to finish failed, err = %v", err)
   478  	}
   479  }
   480  
   481  // TODO -- FIXME: revise and rewrite
   482  //
   483  // Waits for both resilver and rebalance to complete.
   484  // If they were not started, this function treats them as completed
   485  // and returns. If timeout set, if any of rebalances doesn't complete before timeout
   486  // the function ends with fatal.
   487  func WaitForRebalAndResil(t testing.TB, bp api.BaseParams, timeouts ...time.Duration) {
   488  	var (
   489  		wg    = &sync.WaitGroup{}
   490  		errCh = make(chan error, 2)
   491  	)
   492  	smap, err := api.GetClusterMap(bp)
   493  	tassert.CheckFatal(t, err)
   494  
   495  	if nat := smap.CountActiveTs(); nat < 1 {
   496  		// NOTE in re nat == 1: single remaining target vs. graceful shutdown and such
   497  		s := "No targets"
   498  		tlog.Logf("%s, %s - cannot rebalance\n", s, smap)
   499  		_waitResil(t, bp, controlPlaneSleep)
   500  		return
   501  	}
   502  
   503  	_waitReToStart(bp)
   504  	timeout := RebalanceTimeout
   505  	if len(timeouts) > 0 {
   506  		timeout = timeouts[0]
   507  	}
   508  	tlog.Logf("Waiting for rebalance and resilver to complete (timeout %v)\n", timeout)
   509  	wg.Add(2)
   510  	go func() {
   511  		defer wg.Done()
   512  		xargs := xact.ArgsMsg{Kind: apc.ActRebalance, OnlyRunning: true, Timeout: timeout}
   513  		if _, err := api.WaitForXactionIC(bp, &xargs); err != nil {
   514  			if cmn.IsStatusNotFound(err) {
   515  				return
   516  			}
   517  			errCh <- err
   518  		}
   519  	}()
   520  
   521  	go func() {
   522  		defer wg.Done()
   523  		xargs := xact.ArgsMsg{Kind: apc.ActResilver, OnlyRunning: true, Timeout: timeout}
   524  		if _, err := api.WaitForXactionIC(bp, &xargs); err != nil {
   525  			if cmn.IsStatusNotFound(err) {
   526  				return
   527  			}
   528  			errCh <- err
   529  		}
   530  	}()
   531  
   532  	wg.Wait()
   533  	close(errCh)
   534  	for err := range errCh {
   535  		tassert.CheckFatal(t, err)
   536  		return
   537  	}
   538  }
   539  
   540  // compare w/ `tools.WaitForResilvering`
   541  func _waitResil(t testing.TB, bp api.BaseParams, timeout time.Duration) {
   542  	xargs := xact.ArgsMsg{Kind: apc.ActResilver, OnlyRunning: true, Timeout: timeout}
   543  	_, err := api.WaitForXactionIC(bp, &xargs)
   544  	if err == nil {
   545  		return
   546  	}
   547  	if herr, ok := err.(*cmn.ErrHTTP); ok {
   548  		if herr.Status == http.StatusNotFound { // double check iff not found
   549  			time.Sleep(xactPollSleep)
   550  			_, err = api.WaitForXactionIC(bp, &xargs)
   551  		}
   552  	}
   553  	if err == nil {
   554  		return
   555  	}
   556  	if herr, ok := err.(*cmn.ErrHTTP); ok {
   557  		if herr.Status == http.StatusNotFound {
   558  			err = nil
   559  		}
   560  	}
   561  	tassert.CheckError(t, err)
   562  }
   563  
   564  func WaitForRebalanceByID(t *testing.T, bp api.BaseParams, rebID string, timeouts ...time.Duration) {
   565  	if rebID == "" {
   566  		return
   567  	}
   568  	tassert.Fatalf(t, xact.IsValidRebID(rebID), "invalid reb ID %q", rebID)
   569  	timeout := RebalanceTimeout
   570  	if len(timeouts) > 0 {
   571  		timeout = timeouts[0]
   572  	}
   573  	tlog.Logf("Wait for rebalance %s\n", rebID)
   574  	xargs := xact.ArgsMsg{ID: rebID, Kind: apc.ActRebalance, OnlyRunning: true, Timeout: timeout}
   575  	_, err := api.WaitForXactionIC(bp, &xargs)
   576  	tassert.CheckFatal(t, err)
   577  }
   578  
   579  func _waitReToStart(bp api.BaseParams) {
   580  	var (
   581  		kinds   = []string{apc.ActRebalance, apc.ActResilver}
   582  		timeout = max(10*xactPollSleep, MaxCplaneTimeout)
   583  		retries = int(timeout / xactPollSleep)
   584  	)
   585  	for range retries {
   586  		for _, kind := range kinds {
   587  			args := xact.ArgsMsg{Timeout: xactPollSleep, OnlyRunning: true, Kind: kind}
   588  			status, err := api.GetOneXactionStatus(bp, &args)
   589  			if err == nil {
   590  				if !status.Finished() {
   591  					return
   592  				}
   593  			}
   594  		}
   595  		time.Sleep(xactPollSleep)
   596  	}
   597  	tlog.Logf("Warning: timed out (%v) waiting for rebalance or resilver to start\n", timeout)
   598  }
   599  
   600  func GetClusterStats(t *testing.T, proxyURL string) stats.Cluster {
   601  	bp := BaseAPIParams(proxyURL)
   602  	scs, err := api.GetClusterStats(bp)
   603  	tassert.CheckFatal(t, err)
   604  	return scs
   605  }
   606  
   607  func GetNamedStatsVal(ds *stats.Node, name string) int64 {
   608  	v, ok := ds.Tracker[name]
   609  	if !ok {
   610  		return 0
   611  	}
   612  	return v.Value
   613  }
   614  
   615  func GetDaemonConfig(t *testing.T, node *meta.Snode) *cmn.Config {
   616  	bp := BaseAPIParams()
   617  	config, err := api.GetDaemonConfig(bp, node)
   618  	tassert.CheckFatal(t, err)
   619  	return config
   620  }
   621  
   622  func GetClusterMap(tb testing.TB, url string) *meta.Smap {
   623  	smap, err := waitForStartup(BaseAPIParams(url), tb)
   624  	if err == nil && (currSmap == nil || currSmap.Version < smap.Version) {
   625  		currSmap = smap
   626  	}
   627  	return smap
   628  }
   629  
   630  func getClusterConfig() (config *cmn.Config, err error) {
   631  	proxyURL := GetPrimaryURL()
   632  	primary, err := GetPrimaryProxy(proxyURL)
   633  	if err != nil {
   634  		return nil, err
   635  	}
   636  	return api.GetDaemonConfig(BaseAPIParams(proxyURL), primary)
   637  }
   638  
   639  func GetClusterConfig(t *testing.T) (config *cmn.Config) {
   640  	config, err := getClusterConfig()
   641  	tassert.CheckError(t, err)
   642  	return config
   643  }
   644  
   645  func SetClusterConfig(t *testing.T, nvs cos.StrKVs) {
   646  	proxyURL := GetPrimaryURL()
   647  	bp := BaseAPIParams(proxyURL)
   648  	err := api.SetClusterConfig(bp, nvs, false /*transient*/)
   649  	tassert.CheckError(t, err)
   650  }
   651  
   652  func SetClusterConfigUsingMsg(t *testing.T, toUpdate *cmn.ConfigToSet) {
   653  	proxyURL := GetPrimaryURL()
   654  	bp := BaseAPIParams(proxyURL)
   655  	err := api.SetClusterConfigUsingMsg(bp, toUpdate, false /*transient*/)
   656  	tassert.CheckFatal(t, err)
   657  }
   658  
   659  func CheckErrIsNotFound(t *testing.T, err error) {
   660  	if err == nil {
   661  		t.Fatalf("expected error")
   662  		return
   663  	}
   664  	herr, ok := err.(*cmn.ErrHTTP)
   665  	tassert.Fatalf(t, ok, "expected an error of the type *cmn.ErrHTTP, got %v(%T)", err, err)
   666  	tassert.Fatalf(
   667  		t, herr.Status == http.StatusNotFound,
   668  		"expected status: %d, got: %d.", http.StatusNotFound, herr.Status,
   669  	)
   670  }
   671  
   672  func waitForStartup(bp api.BaseParams, ts ...testing.TB) (*meta.Smap, error) {
   673  	for {
   674  		smap, err := api.GetClusterMap(bp)
   675  		if err != nil {
   676  			if api.HTTPStatus(err) == http.StatusServiceUnavailable {
   677  				tlog.Logln("Waiting for the cluster to start up...")
   678  				time.Sleep(WaitClusterStartup)
   679  				continue
   680  			}
   681  
   682  			tlog.Logf("Unable to get usable cluster map: %v\n", err)
   683  			if len(ts) > 0 {
   684  				tassert.CheckFatal(ts[0], err)
   685  			}
   686  			return nil, err
   687  		}
   688  		return smap, nil
   689  	}
   690  }