github.com/cloud-foundations/dominator@v0.0.0-20221004181915-6e4fee580046/cmd/installer/objects.go (about)

     1  // +build linux
     2  
     3  package main
     4  
     5  import (
     6  	"crypto/sha512"
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"os"
    11  	"path/filepath"
    12  	"syscall"
    13  	"time"
    14  
    15  	"github.com/Cloud-Foundations/Dominator/lib/format"
    16  	"github.com/Cloud-Foundations/Dominator/lib/fsutil"
    17  	"github.com/Cloud-Foundations/Dominator/lib/hash"
    18  	"github.com/Cloud-Foundations/Dominator/lib/log"
    19  	"github.com/Cloud-Foundations/Dominator/lib/objectcache"
    20  	"github.com/Cloud-Foundations/Dominator/lib/objectserver"
    21  	"github.com/Cloud-Foundations/Dominator/lib/wsyscall"
    22  )
    23  
    24  type objectsCache struct {
    25  	bytesScanned uint64
    26  	objects      map[hash.Hash]uint64
    27  }
    28  
    29  type objectsReader struct {
    30  	cache  *objectsCache
    31  	hashes []hash.Hash
    32  }
    33  
    34  func hashFile(filename string) (hash.Hash, uint64, error) {
    35  	file, err := os.Open(filename)
    36  	if err != nil {
    37  		return hash.Hash{}, 0, err
    38  	}
    39  	defer file.Close()
    40  	hasher := sha512.New()
    41  	nCopied, err := io.Copy(hasher, file)
    42  	if err != nil {
    43  		return hash.Hash{}, 0, err
    44  	}
    45  	var hashVal hash.Hash
    46  	copy(hashVal[:], hasher.Sum(nil))
    47  	return hashVal, uint64(nCopied), nil
    48  }
    49  
    50  func (cache *objectsCache) computeMissing(
    51  	requiredObjects map[hash.Hash]uint64) (
    52  	map[hash.Hash]uint64, uint64, uint64) {
    53  	var requiredBytes, presentBytes uint64
    54  	missingObjects := make(map[hash.Hash]uint64, len(requiredObjects))
    55  	for hashVal, requiredSize := range requiredObjects {
    56  		requiredBytes += requiredSize
    57  		if size, ok := cache.objects[hashVal]; ok {
    58  			presentBytes += size
    59  		} else {
    60  			missingObjects[hashVal] = requiredSize
    61  		}
    62  	}
    63  	return missingObjects, requiredBytes, presentBytes
    64  }
    65  
    66  func createObjectsCache(requiredObjects map[hash.Hash]uint64,
    67  	objGetter objectserver.ObjectsGetter, rootDevice string,
    68  	logger log.DebugLogger) (*objectsCache, error) {
    69  	cache := &objectsCache{objects: make(map[hash.Hash]uint64)}
    70  	if fi, err := os.Stat(*objectsDirectory); err != nil {
    71  		if !os.IsNotExist(err) {
    72  			return nil, err
    73  		}
    74  		logger.Debugln(0, "scanning root")
    75  		cache.bytesScanned = 0
    76  		startTime := time.Now()
    77  		if err := cache.scanRoot(requiredObjects); err != nil {
    78  			return nil, err
    79  		}
    80  		duration := time.Since(startTime)
    81  		logger.Debugf(0, "scanned root %s in %s (%s/s)\n",
    82  			format.FormatBytes(cache.bytesScanned), format.Duration(duration),
    83  			format.FormatBytes(
    84  				uint64(float64(cache.bytesScanned)/duration.Seconds())))
    85  	} else if !fi.IsDir() {
    86  		return nil,
    87  			fmt.Errorf("%s exists but is not a directory", *objectsDirectory)
    88  	} else {
    89  		if err := cache.scanCache(*objectsDirectory, ""); err != nil {
    90  			return nil, err
    91  		}
    92  	}
    93  	missingObjects, requiredBytes, presentBytes := cache.computeMissing(
    94  		requiredObjects)
    95  	if len(missingObjects) < 1 {
    96  		logger.Debugln(0, "object cache already has all required objects")
    97  		return cache, nil
    98  	}
    99  	logger.Debugf(0, "object cache already has %d/%d objects (%s/%s)\n",
   100  		len(cache.objects), len(requiredObjects),
   101  		format.FormatBytes(presentBytes), format.FormatBytes(requiredBytes))
   102  	err := cache.findAndScanUntrusted(missingObjects, rootDevice, logger)
   103  	if err != nil {
   104  		return nil, err
   105  	}
   106  	err = cache.downloadMissing(requiredObjects, objGetter, logger)
   107  	if err != nil {
   108  		return nil, err
   109  	}
   110  	return cache, nil
   111  }
   112  
   113  func (cache *objectsCache) downloadMissing(requiredObjects map[hash.Hash]uint64,
   114  	objGetter objectserver.ObjectsGetter, logger log.DebugLogger) error {
   115  	missingObjects, _, _ := cache.computeMissing(requiredObjects)
   116  	if len(missingObjects) < 1 {
   117  		return nil
   118  	}
   119  	hashes := make([]hash.Hash, 0, len(missingObjects))
   120  	var totalBytes uint64
   121  	for hashVal, size := range missingObjects {
   122  		hashes = append(hashes, hashVal)
   123  		totalBytes += size
   124  	}
   125  	startTime := time.Now()
   126  	objectsReader, err := objGetter.GetObjects(hashes)
   127  	if err != nil {
   128  		return err
   129  	}
   130  	defer objectsReader.Close()
   131  	for _, hashVal := range hashes {
   132  		if err := cache.getNextObject(hashVal, objectsReader); err != nil {
   133  			return err
   134  		}
   135  	}
   136  	duration := time.Since(startTime)
   137  	logger.Debugf(0, "downloaded %d objects (%s) in %s (%s/s)\n",
   138  		len(missingObjects), format.FormatBytes(totalBytes),
   139  		format.Duration(duration),
   140  		format.FormatBytes(uint64(float64(totalBytes)/duration.Seconds())))
   141  	return nil
   142  }
   143  
   144  func (cache *objectsCache) findAndScanUntrusted(
   145  	requiredObjects map[hash.Hash]uint64, rootDevice string,
   146  	logger log.DebugLogger) error {
   147  	if err := mount(rootDevice, *mountPoint, "ext4", logger); err != nil {
   148  		return nil
   149  	}
   150  	defer syscall.Unmount(*mountPoint, 0)
   151  	logger.Debugln(0, "scanning old root")
   152  	cache.bytesScanned = 0
   153  	startTime := time.Now()
   154  	foundObjects := make(map[hash.Hash]uint64)
   155  	err := cache.scanTree(*mountPoint, true, requiredObjects, foundObjects)
   156  	if err != nil {
   157  		return err
   158  	}
   159  	var requiredBytes, foundBytes uint64
   160  	for _, size := range requiredObjects {
   161  		requiredBytes += size
   162  	}
   163  	for _, size := range foundObjects {
   164  		foundBytes += size
   165  	}
   166  	duration := time.Since(startTime)
   167  	logger.Debugf(0, "scanned old root %s in %s (%s/s)\n",
   168  		format.FormatBytes(cache.bytesScanned), format.Duration(duration),
   169  		format.FormatBytes(
   170  			uint64(float64(cache.bytesScanned)/duration.Seconds())))
   171  	logger.Debugf(0, "found %d/%d objects (%s/%s) in old file-system in %s\n",
   172  		len(foundObjects), len(requiredObjects),
   173  		format.FormatBytes(foundBytes), format.FormatBytes(requiredBytes),
   174  		format.Duration(duration))
   175  	return nil
   176  }
   177  
   178  func (cache *objectsCache) GetObjects(hashes []hash.Hash) (
   179  	objectserver.ObjectsReader, error) {
   180  	return &objectsReader{cache, hashes}, nil
   181  }
   182  
   183  func (cache *objectsCache) getNextObject(hashVal hash.Hash,
   184  	objectsReader objectserver.ObjectsReader) error {
   185  	size, reader, err := objectsReader.NextObject()
   186  	if err != nil {
   187  		return err
   188  	}
   189  	hashName := filepath.Join(*objectsDirectory,
   190  		objectcache.HashToFilename(hashVal))
   191  	if err := os.MkdirAll(filepath.Dir(hashName), fsutil.DirPerms); err != nil {
   192  		return err
   193  	}
   194  	defer reader.Close()
   195  	writer, err := os.Create(hashName)
   196  	if err != nil {
   197  		return err
   198  	}
   199  	defer writer.Close()
   200  	if _, err := io.Copy(writer, reader); err != nil {
   201  		return err
   202  	}
   203  	cache.objects[hashVal] = size
   204  	return nil
   205  }
   206  
   207  func (cache *objectsCache) handleFile(filename string, copy bool,
   208  	requiredObjects, foundObjects map[hash.Hash]uint64) error {
   209  	if hashVal, size, err := hashFile(filename); err != nil {
   210  		return err
   211  	} else if size < 1 {
   212  		return nil
   213  	} else {
   214  		cache.bytesScanned += size
   215  		if _, ok := cache.objects[hashVal]; ok {
   216  			return nil
   217  		}
   218  		if _, ok := requiredObjects[hashVal]; !ok {
   219  			return nil
   220  		}
   221  		cache.objects[hashVal] = size
   222  		if foundObjects != nil {
   223  			foundObjects[hashVal] = size
   224  		}
   225  		hashName := filepath.Join(*objectsDirectory,
   226  			objectcache.HashToFilename(hashVal))
   227  		err := os.MkdirAll(filepath.Dir(hashName), fsutil.DirPerms)
   228  		if err != nil {
   229  			return err
   230  		}
   231  		if copy {
   232  			reader, err := os.Open(filename)
   233  			if err != nil {
   234  				return err
   235  			}
   236  			defer reader.Close()
   237  			writer, err := os.Create(hashName)
   238  			if err != nil {
   239  				return err
   240  			}
   241  			defer writer.Close()
   242  			if _, err := io.Copy(writer, reader); err != nil {
   243  				return err
   244  			}
   245  			return nil
   246  		}
   247  		return os.Symlink(filename, hashName)
   248  	}
   249  }
   250  
   251  func (cache *objectsCache) scanCache(topDir, subpath string) error {
   252  	myPathName := filepath.Join(topDir, subpath)
   253  	file, err := os.Open(myPathName)
   254  	if err != nil {
   255  		return err
   256  	}
   257  	names, err := file.Readdirnames(-1)
   258  	file.Close()
   259  	if err != nil {
   260  		return err
   261  	}
   262  	for _, name := range names {
   263  		pathname := filepath.Join(myPathName, name)
   264  		fi, err := os.Stat(pathname)
   265  		if err != nil {
   266  			return err
   267  		}
   268  		filename := filepath.Join(subpath, name)
   269  		if fi.IsDir() {
   270  			if err := cache.scanCache(topDir, filename); err != nil {
   271  				return err
   272  			}
   273  		} else {
   274  			hashVal, err := objectcache.FilenameToHash(filename)
   275  			if err != nil {
   276  				return err
   277  			}
   278  			cache.objects[hashVal] = uint64(fi.Size())
   279  		}
   280  	}
   281  	return nil
   282  }
   283  
   284  func (cache *objectsCache) scanRoot(
   285  	requiredObjects map[hash.Hash]uint64) error {
   286  	if err := os.Mkdir(*objectsDirectory, fsutil.DirPerms); err != nil {
   287  		return err
   288  	}
   289  	err := wsyscall.Mount("none", *objectsDirectory, "tmpfs", 0, "")
   290  	if err != nil {
   291  		return err
   292  	}
   293  	if err := cache.scanTree("/", false, requiredObjects, nil); err != nil {
   294  		return err
   295  	}
   296  	return nil
   297  }
   298  
   299  func (cache *objectsCache) scanTree(topDir string, copy bool,
   300  	requiredObjects, foundObjects map[hash.Hash]uint64) error {
   301  	var rootStat syscall.Stat_t
   302  	if err := syscall.Lstat(topDir, &rootStat); err != nil {
   303  		return err
   304  	}
   305  	return cache.walk(topDir, rootStat.Dev, copy, requiredObjects, foundObjects)
   306  }
   307  
   308  func (cache *objectsCache) walk(dirname string, device uint64, copy bool,
   309  	requiredObjects, foundObjects map[hash.Hash]uint64) error {
   310  	file, err := os.Open(dirname)
   311  	if err != nil {
   312  		return err
   313  	}
   314  	names, err := file.Readdirnames(-1)
   315  	file.Close()
   316  	if err != nil {
   317  		return err
   318  	}
   319  	for _, name := range names {
   320  		pathname := filepath.Join(dirname, name)
   321  		var stat syscall.Stat_t
   322  		err := syscall.Lstat(pathname, &stat)
   323  		if err != nil {
   324  			return err
   325  		}
   326  		if stat.Mode&syscall.S_IFMT == syscall.S_IFDIR {
   327  			if stat.Dev != device {
   328  				continue
   329  			}
   330  			err := cache.walk(pathname, device, copy, requiredObjects,
   331  				foundObjects)
   332  			if err != nil {
   333  				return err
   334  			}
   335  		} else if stat.Mode&syscall.S_IFMT == syscall.S_IFREG {
   336  			err := cache.handleFile(pathname, copy, requiredObjects,
   337  				foundObjects)
   338  			if err != nil {
   339  				return err
   340  			}
   341  		}
   342  	}
   343  	return nil
   344  }
   345  
   346  func (or *objectsReader) Close() error {
   347  	return nil
   348  }
   349  
   350  func (or *objectsReader) NextObject() (uint64, io.ReadCloser, error) {
   351  	if len(or.hashes) < 1 {
   352  		return 0, nil, errors.New("all objects have been consumed")
   353  	}
   354  	hashVal := or.hashes[0]
   355  	or.hashes = or.hashes[1:]
   356  	hashName := filepath.Join(*objectsDirectory,
   357  		objectcache.HashToFilename(hashVal))
   358  	if file, err := os.Open(hashName); err != nil {
   359  		return 0, nil, err
   360  	} else {
   361  		return or.cache.objects[hashVal], file, nil
   362  	}
   363  }