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 }