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

     1  // Package volume provides volume (a.k.a. pool of disks) abstraction and methods to configure, store,
     2  // and validate the corresponding metadata. AIS volume is built on top of mountpaths (fs package).
     3  /*
     4   * Copyright (c) 2018-2024, NVIDIA CORPORATION. All rights reserved.
     5   */
     6  package volume
     7  
     8  import (
     9  	"errors"
    10  	"fmt"
    11  	"os"
    12  
    13  	"github.com/NVIDIA/aistore/cmn"
    14  	"github.com/NVIDIA/aistore/cmn/cos"
    15  	"github.com/NVIDIA/aistore/cmn/debug"
    16  	"github.com/NVIDIA/aistore/cmn/nlog"
    17  	"github.com/NVIDIA/aistore/core"
    18  	"github.com/NVIDIA/aistore/fs"
    19  	"github.com/NVIDIA/aistore/ios"
    20  )
    21  
    22  type IniCtx struct {
    23  	UseLoopbacks  bool // using loopback dev-s
    24  	IgnoreMissing bool // ignore missing mountpath(s)
    25  	RandomTID     bool // generated random target ID
    26  }
    27  
    28  // bootstrap from local-config referenced locations
    29  // NOTE: local plain-text config is kept in-sync with mountpath changes (see ais/fspathgrp)
    30  // potential TODO for bare-metal deployments: store persistent labels (dmidecode style)
    31  
    32  // - load (or initialize new) volume
    33  // - initialize mountpaths
    34  // - check a variety of SIE (storage integrity error) conditions; terminate and exit if detected
    35  func Init(t core.Target, config *cmn.Config, ctx IniCtx) (created bool) {
    36  	var (
    37  		vmd     *VMD
    38  		tid     = t.SID()
    39  		fspaths = config.FSP.Paths.Keys()
    40  	)
    41  	// new and empty
    42  	fs.New(len(config.FSP.Paths))
    43  
    44  	if v, err := configLoadVMD(tid, config.FSP.Paths); err != nil {
    45  		cos.ExitLogf("%s: %v (config-load-vmd, %v)", t, err, fspaths)
    46  	} else {
    47  		vmd = v
    48  	}
    49  
    50  	// a) the very first deployment when volume does not exist, or
    51  	// b) when the config doesn't contain a single valid mountpath
    52  	// (that in turn contains a copy of VMD, possibly outdated (but that's ok))
    53  	if vmd == nil {
    54  		if err := configInitMPI(tid, config); err != nil {
    55  			cos.ExitLogf("%s: %v (config-init-mpi, %v, %+v)", t, err, fspaths, ctx)
    56  		}
    57  		nlog.Warningln(t.String()+":", "creating new VMD from", fspaths, "config")
    58  		if v, err := NewFromMPI(tid); err != nil {
    59  			cos.ExitLogf("%s: %v (new-from-mpi)", t, err) // unlikely
    60  		} else {
    61  			vmd = v
    62  		}
    63  		nlog.Warningln(t.String()+":", vmd.String(), "initialized")
    64  		created = true
    65  		return
    66  	}
    67  
    68  	// otherwise, use loaded VMD to find the most recently updated (the current) one and, simultaneously,
    69  	// initialize MPI
    70  	var persist bool
    71  	if v, haveOld, err := initMPI(tid, config, vmd, 1 /*pass #1*/, ctx.IgnoreMissing); err != nil {
    72  		cos.ExitLogf("%s: %v (vmd-init-mpi-p1, %+v, %s)", t, err, ctx, vmd)
    73  	} else {
    74  		if v != nil && v.Version > vmd.Version {
    75  			vmd = v
    76  			persist = true
    77  		}
    78  		if haveOld {
    79  			persist = true
    80  		}
    81  		if v, _, err := initMPI(tid, config, vmd, 2 /*pass #2*/, ctx.IgnoreMissing); err != nil {
    82  			cos.ExitLogf("%s: %v (vmd-init-mpi-p2, have-old=%t, %+v, %s)", t, err, haveOld, ctx, vmd)
    83  		} else {
    84  			debug.Assert(v == nil || v.Version == vmd.Version)
    85  		}
    86  		if persist {
    87  			vmd.persist()
    88  		}
    89  	}
    90  
    91  	nlog.Infoln(vmd.String())
    92  	return
    93  }
    94  
    95  // MPI => VMD
    96  func NewFromMPI(tid string) (vmd *VMD, err error) {
    97  	var (
    98  		curVersion          uint64
    99  		available, disabled = fs.Get()
   100  	)
   101  	vmd, err = loadVMD(tid, nil)
   102  	if err != nil {
   103  		nlog.Warningln(err) // TODO: handle
   104  	}
   105  	if vmd != nil {
   106  		curVersion = vmd.Version
   107  	}
   108  	vmd = newVMD(len(available))
   109  	vmd.DaemonID = tid
   110  	vmd.Version = curVersion + 1 // Bump the version.
   111  	for _, mi := range available {
   112  		vmd.addMountpath(mi, true /*enabled*/)
   113  	}
   114  	for _, mi := range disabled {
   115  		vmd.addMountpath(mi, false /*enabled*/)
   116  	}
   117  	err = vmd.persist()
   118  	return
   119  }
   120  
   121  func newVMD(expectedSize int) *VMD {
   122  	return &VMD{Mountpaths: make(map[string]*fsMpathMD, expectedSize)}
   123  }
   124  
   125  // local config => fs.MPI
   126  func configInitMPI(tid string, config *cmn.Config) (err error) {
   127  	var (
   128  		fspaths  = config.FSP.Paths
   129  		avail    = make(fs.MPI, len(fspaths))
   130  		disabled = make(fs.MPI)
   131  	)
   132  	for path, label := range fspaths {
   133  		var mi *fs.Mountpath
   134  		if mi, err = fs.NewMountpath(path, ios.Label(label)); err != nil {
   135  			goto rerr
   136  		}
   137  		if err = mi.AddEnabled(tid, avail, config); err != nil {
   138  			goto rerr
   139  		}
   140  	}
   141  	if len(avail) == 0 {
   142  		err = cmn.ErrNoMountpaths
   143  		goto rerr
   144  	}
   145  	fs.PutMPI(avail, disabled)
   146  	return
   147  
   148  rerr:
   149  	err = cmn.NewErrInvalidFSPathsConf(err)
   150  	return
   151  }
   152  
   153  // VMD => fs.MPI in two passes
   154  func initMPI(tid string, config *cmn.Config, vmd *VMD, pass int, ignoreMissingMi bool) (maxVer *VMD, haveOld bool, err error) {
   155  	var (
   156  		avail    = make(fs.MPI, len(vmd.Mountpaths))
   157  		disabled = make(fs.MPI)
   158  	)
   159  	debug.Assert(vmd.DaemonID == tid)
   160  
   161  	for mpath, fsMpathMD := range vmd.Mountpaths {
   162  		var mi *fs.Mountpath
   163  		mi, err = fs.NewMountpath(mpath, fsMpathMD.Label)
   164  		if !fsMpathMD.Enabled {
   165  			if pass == 2 {
   166  				mi.Fs = fsMpathMD.Fs
   167  				mi.FsType = fsMpathMD.FsType
   168  				mi.FsID = fsMpathMD.FsID
   169  				mi.AddDisabled(disabled)
   170  			}
   171  			continue
   172  		}
   173  		// enabled
   174  		if err != nil {
   175  			err = &fs.ErrStorageIntegrity{Code: fs.SieMpathNotFound, Msg: err.Error()}
   176  			if pass == 1 || ignoreMissingMi {
   177  				nlog.Errorf("%v (pass=%d, ignore-missing=%t)", err, pass, ignoreMissingMi)
   178  				err = nil
   179  				continue
   180  			}
   181  			return
   182  		}
   183  		if mi.Path != mpath {
   184  			nlog.Warningf("%s: cleanpath(%q) => %q", mi, mpath, mi.Path)
   185  		}
   186  
   187  		// The (mountpath => filesystem) relationship is persistent and must _not_ change upon reboot.
   188  		// There are associated false positives, though, namely:
   189  		// 1. FS ID change. Reason: certain filesystems simply do not maintain persistence (of their IDs).
   190  		// 2. `Fs` (usually, device name) change. Reason: OS block-level subsystem enumerated devices
   191  		//    in a different order.
   192  		// NOTE: no workaround if (1) and (2) happen simultaneously (must be extremely unlikely).
   193  		//
   194  		// See also: `allowSharedDisksAndNoDisks` and `startWithLostMountpath`
   195  		if mi.FsType != fsMpathMD.FsType || mi.Fs != fsMpathMD.Fs || mi.FsID != fsMpathMD.FsID {
   196  			if mi.FsType != fsMpathMD.FsType || (mi.Fs != fsMpathMD.Fs && mi.FsID != fsMpathMD.FsID) {
   197  				err = &fs.ErrStorageIntegrity{
   198  					Code: fs.SieFsDiffers,
   199  					Msg:  fmt.Sprintf("lost or missing mountpath %q (%+v vs %+v)", mpath, mi.FS, *fsMpathMD),
   200  				}
   201  				if pass == 1 || ignoreMissingMi {
   202  					nlog.Errorf("%v (pass=%d, ignore-missing=%t)", err, pass, ignoreMissingMi)
   203  					err = nil
   204  					continue
   205  				}
   206  				return
   207  			}
   208  			if mi.Fs == fsMpathMD.Fs && mi.FsID != fsMpathMD.FsID {
   209  				nlog.Warningf("detected FS ID change: mp=%q, curr=%+v, prev=%+v (pass %d)",
   210  					mpath, mi.FS, *fsMpathMD, pass)
   211  			} else if mi.Fs != fsMpathMD.Fs && mi.FsID == fsMpathMD.FsID {
   212  				nlog.Warningf("detected device name change for the same FS ID: mp=%q, curr=%+v, prev=%+v (pass %d)",
   213  					mpath, mi.FS, *fsMpathMD, pass)
   214  			}
   215  		}
   216  
   217  		if pass == 1 {
   218  			if v, old, errLoad := loadOneVMD(tid, vmd, mi.Path, len(vmd.Mountpaths)); v != nil {
   219  				debug.Assert(v.Version > vmd.Version)
   220  				maxVer = v
   221  			} else if old {
   222  				debug.AssertNoErr(errLoad)
   223  				haveOld = true
   224  			} else if errLoad != nil {
   225  				nlog.Warningf("%s: %v", mi, errLoad)
   226  			}
   227  		} else {
   228  			if err = mi.AddEnabled(tid, avail, config); err != nil {
   229  				return
   230  			}
   231  		}
   232  	}
   233  
   234  	if pass == 1 {
   235  		return
   236  	}
   237  	if len(avail) == 0 {
   238  		if len(disabled) == 0 {
   239  			err = cmn.ErrNoMountpaths
   240  			return
   241  		}
   242  		nlog.Errorf("Warning: %v (avail=%d, disabled=%d)", err, len(avail), len(disabled))
   243  	}
   244  	fs.PutMPI(avail, disabled)
   245  	// TODO: insufficient
   246  	if la, lc := len(avail), len(config.FSP.Paths); la != lc {
   247  		nlog.Warningf("number of available mountpaths (%d) differs from the configured (%d)", la, lc)
   248  		nlog.Warningln("run 'ais storage mountpath [attach|detach]', fix the config, or ignore")
   249  	}
   250  	return
   251  }
   252  
   253  // pre-loading to try to recover lost tid
   254  func RecoverTID(generatedID string, configPaths cos.StrKVs) (tid string, recovered bool) {
   255  	available := make(fs.MPI, len(configPaths)) // temp MPI to attempt loading
   256  	for mpath := range configPaths {
   257  		available[mpath] = nil
   258  	}
   259  	vmd := newVMD(len(available))
   260  	for mpath := range available {
   261  		if err := vmd.load(mpath); err != nil {
   262  			continue
   263  		}
   264  		debug.Assert(vmd.DaemonID != "" && vmd.DaemonID != generatedID)
   265  		if tid == "" {
   266  			nlog.Warningf("recovered lost target ID %q from mpath %s", vmd.DaemonID, mpath)
   267  			tid = vmd.DaemonID
   268  			recovered = true
   269  		} else if tid != vmd.DaemonID {
   270  			cos.ExitLogf("multiple conflicting target IDs %q(%q) vs %q", vmd.DaemonID, mpath, tid) // FATAL
   271  		}
   272  	}
   273  	if tid == "" {
   274  		tid = generatedID
   275  	}
   276  	return tid, recovered
   277  }
   278  
   279  // loading
   280  
   281  func LoadVMDTest() (*VMD, error) { return loadVMD("", nil) } // test-only
   282  
   283  // config => (temp MPI) => VMD
   284  func configLoadVMD(tid string, configPaths cos.StrKVs) (vmd *VMD, err error) {
   285  	if len(configPaths) == 0 {
   286  		err = errors.New("no fspaths - see README => Configuration and fspaths section in the config.sh")
   287  		return
   288  	}
   289  	available := make(fs.MPI, len(configPaths)) // temp MPI to attempt loading
   290  	for mpath := range configPaths {
   291  		available[mpath] = nil
   292  	}
   293  	return loadVMD(tid, available)
   294  }
   295  
   296  // given a set of *available mountpaths* loadVMD discovers, loads, and validates
   297  // the most recently updated VMD (which is stored in several copies for redundancy).
   298  // - Returns nil if VMD does not exist;
   299  // - Returns error on failure to validate or load existing VMD.
   300  func loadVMD(tid string, available fs.MPI) (vmd *VMD, err error) {
   301  	if available == nil {
   302  		available = fs.GetAvail()
   303  	}
   304  	l := len(available)
   305  	for mpath := range available {
   306  		var v *VMD
   307  		v, _, err = loadOneVMD(tid, vmd, mpath, l)
   308  		if err != nil {
   309  			return
   310  		}
   311  		if v != nil {
   312  			vmd = v
   313  		}
   314  	}
   315  	return
   316  }
   317  
   318  // given mountpath return a greater-version VMD if available
   319  func loadOneVMD(tid string, vmd *VMD, mpath string, l int) (*VMD, bool /*have old*/, error) {
   320  	var (
   321  		v   = newVMD(l)
   322  		err = v.load(mpath)
   323  	)
   324  	if err != nil {
   325  		if os.IsNotExist(err) {
   326  			return nil, false, nil
   327  		}
   328  		return nil, false, &fs.ErrStorageIntegrity{
   329  			Code: fs.SieMetaCorrupted,
   330  			Msg:  fmt.Sprintf("failed to load VMD from %q: %v", mpath, err),
   331  		}
   332  	}
   333  	if vmd == nil {
   334  		if tid != "" && v.DaemonID != tid {
   335  			return nil, false, &fs.ErrStorageIntegrity{
   336  				Code: fs.SieTargetIDMismatch,
   337  				Msg:  fmt.Sprintf("%s has a different target ID: %q != %q", v, v.DaemonID, tid),
   338  			}
   339  		}
   340  		return v, false, nil
   341  	}
   342  	//
   343  	// validate
   344  	//
   345  	debug.Assert(vmd.DaemonID == tid || tid == "")
   346  	if v.DaemonID != vmd.DaemonID {
   347  		return nil, false, &fs.ErrStorageIntegrity{
   348  			Code: fs.SieTargetIDMismatch,
   349  			Msg:  fmt.Sprintf("%s has a different target ID: %q != %q", v, v.DaemonID, vmd.DaemonID),
   350  		}
   351  	}
   352  	if v.Version > vmd.Version {
   353  		if !_mpathGreaterEq(v, vmd, mpath) {
   354  			nlog.Warningf("mpath %s stores newer VMD: %s > %s", mpath, v, vmd)
   355  		}
   356  		return v, false, nil
   357  	}
   358  	if v.Version < vmd.Version {
   359  		if !_mpathGreaterEq(vmd, v, mpath) {
   360  			md := vmd.Mountpaths[mpath]
   361  			// warn of an older version only if this mpath is enabled in the newer one
   362  			if md != nil && md.Enabled {
   363  				nlog.Warningf("mpath %s stores older VMD: %s < %s", mpath, v, vmd)
   364  			}
   365  		}
   366  		return nil, true, nil // true: outdated copy that must be updated
   367  	}
   368  	if !v.equal(vmd) { // same version must be identical
   369  		err = &fs.ErrStorageIntegrity{
   370  			Code: fs.SieNotEqVMD,
   371  			Msg:  fmt.Sprintf("same VMD versions must be identical: %s(mpath %q) vs %s", v, mpath, vmd),
   372  		}
   373  	}
   374  	return nil, false, err
   375  }