github.com/jaypipes/ghw@v0.21.1/pkg/snapshot/clonetree.go (about)

     1  //
     2  // Use and distribution licensed under the Apache license version 2.
     3  //
     4  // See the COPYING file in the root project directory for full text.
     5  //
     6  
     7  package snapshot
     8  
     9  import (
    10  	"errors"
    11  	"os"
    12  	"path/filepath"
    13  	"strings"
    14  )
    15  
    16  // Attempting to tar up pseudofiles like /proc/cpuinfo is an exercise in
    17  // futility. Notably, the pseudofiles, when read by syscalls, do not return the
    18  // number of bytes read. This causes the tar writer to write zero-length files.
    19  //
    20  // Instead, it is necessary to build a directory structure in a tmpdir and
    21  // create actual files with copies of the pseudofile contents
    22  
    23  // CloneTreeInto copies all the pseudofiles that ghw will consume into the root
    24  // `scratchDir`, preserving the hieratchy.
    25  func CloneTreeInto(scratchDir string) error {
    26  	err := setupScratchDir(scratchDir)
    27  	if err != nil {
    28  		return err
    29  	}
    30  	fileSpecs := ExpectedCloneContent()
    31  	return CopyFilesInto(fileSpecs, scratchDir, nil)
    32  }
    33  
    34  // ExpectedCloneContent return a slice of glob patterns which represent the pseudofiles
    35  // ghw cares about.
    36  // The intended usage of this function is to validate a clone tree, checking that the
    37  // content matches the expectations.
    38  // Beware: the content is host-specific, because the content pertaining some subsystems,
    39  // most notably PCI, is host-specific and unpredictable.
    40  func ExpectedCloneContent() []string {
    41  	fileSpecs := ExpectedCloneStaticContent()
    42  	fileSpecs = append(fileSpecs, ExpectedCloneNetContent()...)
    43  	fileSpecs = append(fileSpecs, ExpectedCloneUSBContent()...)
    44  	fileSpecs = append(fileSpecs, ExpectedClonePCIContent()...)
    45  	fileSpecs = append(fileSpecs, ExpectedCloneGPUContent()...)
    46  	return fileSpecs
    47  }
    48  
    49  // ValidateClonedTree checks the content of a cloned tree, whose root is `clonedDir`,
    50  // against a slice of glob specs which must be included in the cloned tree.
    51  // Is not wrong, and this functions doesn't enforce this, that the cloned tree includes
    52  // more files than the necessary; ghw will just ignore the files it doesn't care about.
    53  // Returns a slice of glob patters expected (given) but not found in the cloned tree,
    54  // and the error during the validation (if any).
    55  func ValidateClonedTree(fileSpecs []string, clonedDir string) ([]string, error) {
    56  	missing := []string{}
    57  	for _, fileSpec := range fileSpecs {
    58  		matches, err := filepath.Glob(filepath.Join(clonedDir, fileSpec))
    59  		if err != nil {
    60  			return missing, err
    61  		}
    62  		if len(matches) == 0 {
    63  			missing = append(missing, fileSpec)
    64  		}
    65  	}
    66  	return missing, nil
    67  }
    68  
    69  // CopyFileOptions allows to finetune the behaviour of the CopyFilesInto function
    70  type CopyFileOptions struct {
    71  	// IsSymlinkFn allows to control the behaviour when handling a symlink.
    72  	// If this hook returns true, the source file is treated as symlink: the cloned
    73  	// tree will thus contain a symlink, with its path adjusted to match the relative
    74  	// path inside the cloned tree. If return false, the symlink will be deferred.
    75  	// The easiest use case of this hook is if you want to avoid symlinks in your cloned
    76  	// tree (having duplicated content). In this case you can just add a function
    77  	// which always return false.
    78  	IsSymlinkFn func(path string, info os.FileInfo) bool
    79  	// ShouldCreateDirFn allows to control if empty directories listed as clone
    80  	// content should be created or not. When creating snapshots, empty directories
    81  	// are most often useless (but also harmless). Because of this, directories are only
    82  	// created as side effect of copying the files which are inside, and thus directories
    83  	// are never empty. The only notable exception are device driver on linux: in this
    84  	// case, for a number of technical/historical reasons, we care about the directory
    85  	// name, but not about the files which are inside.
    86  	// Hence, this is the only case on which ghw clones empty directories.
    87  	ShouldCreateDirFn func(path string, info os.FileInfo) bool
    88  }
    89  
    90  // CopyFilesInto copies all the given glob files specs in the given `destDir` directory,
    91  // preserving the directory structure. This means you can provide a deeply nested filespec
    92  // like
    93  // - /some/deeply/nested/file*
    94  // and you DO NOT need to build the tree incrementally like
    95  // - /some/
    96  // - /some/deeply/
    97  // ...
    98  // all glob patterns supported in `filepath.Glob` are supported.
    99  func CopyFilesInto(fileSpecs []string, destDir string, opts *CopyFileOptions) error {
   100  	if opts == nil {
   101  		opts = &CopyFileOptions{
   102  			IsSymlinkFn:       isSymlink,
   103  			ShouldCreateDirFn: isDriversDir,
   104  		}
   105  	}
   106  	for _, fileSpec := range fileSpecs {
   107  		trace("copying spec: %q\n", fileSpec)
   108  		matches, err := filepath.Glob(fileSpec)
   109  		if err != nil {
   110  			return err
   111  		}
   112  		if err := copyFileTreeInto(matches, destDir, opts); err != nil {
   113  			return err
   114  		}
   115  	}
   116  	return nil
   117  }
   118  
   119  func copyFileTreeInto(paths []string, destDir string, opts *CopyFileOptions) error {
   120  	for _, path := range paths {
   121  		trace("  copying path: %q\n", path)
   122  		baseDir := filepath.Dir(path)
   123  		if err := os.MkdirAll(filepath.Join(destDir, baseDir), os.ModePerm); err != nil {
   124  			return err
   125  		}
   126  
   127  		fi, err := os.Lstat(path)
   128  		if err != nil {
   129  			return err
   130  		}
   131  		// directories must be listed explicitly and created separately.
   132  		// In the future we may want to expose this decision as hook point in
   133  		// CopyFileOptions, when clear use cases emerge.
   134  		destPath := filepath.Join(destDir, path)
   135  		if fi.IsDir() {
   136  			if opts.ShouldCreateDirFn(path, fi) {
   137  				if err := os.MkdirAll(destPath, os.ModePerm); err != nil {
   138  					return err
   139  				}
   140  			} else {
   141  				trace("expanded glob path %q is a directory - skipped\n", path)
   142  			}
   143  			continue
   144  		}
   145  		if opts.IsSymlinkFn(path, fi) {
   146  			trace("    copying link: %q -> %q\n", path, destPath)
   147  			if err := copyLink(path, destPath); err != nil {
   148  				return err
   149  			}
   150  		} else {
   151  			trace("    copying file: %q -> %q\n", path, destPath)
   152  			if err := copyPseudoFile(path, destPath); err != nil && !errors.Is(err, os.ErrPermission) {
   153  				return err
   154  			}
   155  		}
   156  	}
   157  	return nil
   158  }
   159  
   160  func isSymlink(path string, fi os.FileInfo) bool {
   161  	return fi.Mode()&os.ModeSymlink != 0
   162  }
   163  
   164  func isDriversDir(path string, fi os.FileInfo) bool {
   165  	return strings.Contains(path, "drivers")
   166  }
   167  
   168  func copyLink(path, targetPath string) error {
   169  	target, err := os.Readlink(path)
   170  	if err != nil {
   171  		return err
   172  	}
   173  	trace("      symlink %q -> %q\n", target, targetPath)
   174  	if err := os.Symlink(target, targetPath); err != nil {
   175  		if errors.Is(err, os.ErrExist) {
   176  			return nil
   177  		}
   178  		return err
   179  	}
   180  
   181  	return nil
   182  }
   183  
   184  func copyPseudoFile(path, targetPath string) error {
   185  	buf, err := os.ReadFile(path)
   186  	if err != nil {
   187  		return err
   188  	}
   189  	trace("creating %s\n", targetPath)
   190  	f, err := os.Create(targetPath)
   191  	if err != nil {
   192  		return err
   193  	}
   194  	if _, err = f.Write(buf); err != nil {
   195  		return err
   196  	}
   197  	f.Close()
   198  	return nil
   199  }