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 }