Change:
<!-- Please use this template while reporting a bug and provide as much info as possible. Not doing so may result in your bug not being addressed in a timely manner. Thanks! If the matter is security related, please disclose it privately via --What happened: in calculates number of inodes wrong when there are hard links. What you expected to happen: inodes with hardlinks should only be counted once. How to reproduce it (as minimally and precisely as possible): Anything else we need to know?: Environment: Kubernetes version (use ):Cloud provider or hardware configuration: OS (e.g: ):Kernel (e.g. ):Install tools: Network plugin and version (if this is a network-related bug): Others:
This issue is currently awaiting triage. If a SIG or subproject determines this is a relevant issue, they will accept it by applying the label and provide further guidance. The label can be by org members by writing in a comment. <detailsInstructions for interacting with me using PR comments are available . If you have questions or suggestions related to my behavior, please file an issue against the repository. </details
/sig storage
Issues go stale after 90d of inactivity. Mark the issue as fresh with . Stale issues rot after an additional 30d of inactivity and eventually close. If this issue is safe to close now please do so with . Send feedback to sig-testing, kubernetes/test-infra and/or . /lifecycle stale
Stale issues rot after 30d of inactivity. Mark the issue as fresh with . Rotten issues close after an additional 30d of inactivity. If this issue is safe to close now please do so with . Send feedback to sig-contributor-experience at . /lifecycle rotten
Issue is still not solved. There is an open PR waiting for review. /remove-lifecycle rotten
Issues go stale after 90d of inactivity. Mark the issue as fresh with . Stale issues rot after an additional 30d of inactivity and eventually close. If this issue is safe to close now please do so with . Send feedback to sig-contributor-experience at . /lifecycle stale
/remove-lifecycle stale
still waiting for to be merged

Discussion:
return metrics, NewNoPathDefinedError() } <del> err := md.runDiskUsage(metrics) if err != nil { return metrics, err } err = md.runFind(metrics) </del> <ins> err := md.getDiskUsage(metrics) </ins> if err != nil { return metrics, err }

return metrics, nil } <del> // runDiskUsage gets disk usage of md.path and writes the results to metrics.Used func (md *metricsDu) runDiskUsage(metrics *Metrics) error { used, err := fs.DiskUsage(md.path) if err != nil { return err } metrics.Used = used return nil } // runFind executes the "find" command and writes the results to metrics.InodesUsed func (md *metricsDu) runFind(metrics *Metrics) error { inodesUsed, err := fs.Find(md.path) </del> <ins> // getDiskUsage writes metrics.Used and metric.InodesUsed from fs.DiskUsage func (md *metricsDu) getDiskUsage(metrics *Metrics) error { usage, err := fs.DiskUsage(md.path) </ins> if err != nil { return err } <del> metrics.InodesUsed = resource.NewQuantity(inodesUsed, resource.BinarySI) </del> <ins> metrics.Used = resource.NewQuantity(usage.Bytes, resource.BinarySI) metrics.InodesUsed = resource.NewQuantity(usage.Inodes, resource.BinarySI) </ins> return nil }

if e, a := (expectedEmptyDirUsage.Value() + getExpectedBlockSize(filepath.Join(tmpDir, "f1"))), actual.Used.Value(); e != a { t.Errorf("Unexpected Used for directory with file.  Expected %v, got %d.", e, a) } <ins> // create a hardlink and expect inodes count to stay the same previousInodes := actual.InodesUsed.Value() err = os.Link(filepath.Join(tmpDir, "f1"), filepath.Join(tmpDir, "f2")) if err != nil { t.Errorf("Unexpected error when creating hard link %v", err) } actual, err = metrics.GetMetrics() if err != nil { t.Errorf("Unexpected error when calling GetMetrics %v", err) } if e, a := previousInodes, actual.InodesUsed.Value(); e != a { t.Errorf("Unexpected Used for directory with file.  Expected %v, got %d.", e, a) } </ins> } // TestMetricsDuRequireInit tests that if MetricsDu is not initialized with a path, GetMetrics

package fs import ( <del> "bytes" </del> "fmt" <del> "os/exec" "strings" </del> <ins> "os" "path/filepath" "syscall" </ins> "golang.org/x/sys/unix" <del> "k8s.io/apimachinery/pkg/api/resource" </del> "k8s.io/kubernetes/pkg/volume/util/fsquota" ) <ins> type UsageInfo struct { Bytes  int64 Inodes int64 } </ins> // Info linux returns (available bytes, byte capacity, byte usage, total inodes, inodes free, inode usage, error) // for the filesystem that path resides upon. func Info(path string) (int64, int64, int64, int64, int64, int64, error) {

return available, capacity, usage, inodes, inodesFree, inodesUsed, nil } <del> // DiskUsage gets disk usage of specified path. func DiskUsage(path string) (*resource.Quantity, error) { // First check whether the quota system knows about this directory // A nil quantity with no error means that the path does not support quotas // and we should use other mechanisms. data, err := fsquota.GetConsumption(path) if data != nil { return data, nil } else if err != nil { return nil, fmt.Errorf("unable to retrieve disk consumption via quota for %s: %v", path, err) } // Uses the same niceness level as cadvisor.fs does when running du // Uses -B 1 to always scale to a blocksize of 1 byte out, err := exec.Command("nice", "-n", "19", "du", "-x", "-s", "-B", "1", path).CombinedOutput() if err != nil { return nil, fmt.Errorf("failed command 'du' ($ nice -n 19 du -x -s -B 1) on path %s with error %v", path, err) } used, err := resource.ParseQuantity(strings.Fields(string(out))[0]) if err != nil { return nil, fmt.Errorf("failed to parse 'du' output %s due to error %v", out, err) } used.Format = resource.BinarySI return &used, nil } </del> <ins> // DiskUsage calculates the number of inodes and disk usage for a given directory func DiskUsage(path string) (UsageInfo, error) { var usage UsageInfo </ins> <del> // Find uses the equivalent of the command `find <path> -dev -printf '.' | wc -c` to count files and directories. // While this is not an exact measure of inodes used, it is a very good approximation. func Find(path string) (int64, error) { </del> if path == "" { <del> return 0, fmt.Errorf("invalid directory") </del> <ins> return usage, fmt.Errorf("invalid directory") </ins> } // First check whether the quota system knows about this directory <del> // A nil quantity with no error means that the path does not support quotas // and we should use other mechanisms. inodes, err := fsquota.GetInodes(path) if inodes != nil { return inodes.Value(), nil } else if err != nil { return 0, fmt.Errorf("unable to retrieve inode consumption via quota for %s: %v", path, err) </del> <ins> // A nil quantity or error means that the path does not support quotas // or xfs_quota tool is missing and we should use other mechanisms. consumption, _ := fsquota.GetConsumption(path) if consumption != nil { usage.Bytes = consumption.Value() </ins> } <del> var counter byteCounter var stderr bytes.Buffer findCmd := exec.Command("find", path, "-xdev", "-printf", ".") findCmd.Stdout, findCmd.Stderr = &counter, &stderr if err := findCmd.Start(); err != nil { return 0, fmt.Errorf("failed to exec cmd %v - %v; stderr: %v", findCmd.Args, err, stderr.String()) </del> <ins> inodes, _ := fsquota.GetInodes(path) if inodes != nil { usage.Inodes = inodes.Value() </ins> } <del> if err := findCmd.Wait(); err != nil { return 0, fmt.Errorf("cmd %v failed. stderr: %s; err: %v", findCmd.Args, stderr.String(), err) </del> <ins> if inodes != nil && consumption != nil { return usage, nil </ins> } <del> return counter.bytesWritten, nil } // Simple io.Writer implementation that counts how many bytes were written. type byteCounter struct{ bytesWritten int64 } </del> <ins> topLevelStat := &unix.Stat_t{} err := unix.Stat(path, topLevelStat) if err != nil { return usage, err } </ins> <del> func (b *byteCounter) Write(p []byte) (int, error) { b.bytesWritten += int64(len(p)) return len(p), nil </del> <ins> // dedupedInode stores inodes that could be duplicates (nlink > 1) dedupedInodes := make(map[uint64]struct{}) err = filepath.Walk(path, func(path string, info os.FileInfo, err error) error { // ignore files that have been deleted after directory was read if os.IsNotExist(err) { return nil } if err != nil { return fmt.Errorf("unable to count inodes for %s: %s", path, err) } // according to the docs, Sys can be nil if info.Sys() == nil { return fmt.Errorf("fileinfo Sys is nil") } s, ok := info.Sys().(*syscall.Stat_t) if !ok { return fmt.Errorf("unsupported fileinfo; could not convert to stat_t") } if s.Dev != topLevelStat.Dev { // don't descend into directories on other devices return filepath.SkipDir } // Dedupe hardlinks if s.Nlink > 1 { if _, ok := dedupedInodes[s.Ino]; !ok { dedupedInodes[s.Ino] = struct{}{} } else { return nil } } if consumption == nil { usage.Bytes += int64(s.Blocks) * int64(512) // blocksize in bytes } if inodes == nil { usage.Inodes++ } return nil }) return usage, err </ins> }

import ( "fmt" <del> "k8s.io/apimachinery/pkg/api/resource" </del> ) <ins> type UsageInfo struct { Bytes  int64 Inodes int64 } </ins> // Info unsupported returns 0 values for available and capacity and an error. func Info(path string) (int64, int64, int64, int64, int64, int64, error) { return 0, 0, 0, 0, 0, 0, fmt.Errorf("fsinfo not supported for this build") } // DiskUsage gets disk usage of specified path. <del> func DiskUsage(path string) (*resource.Quantity, error) { return nil, fmt.Errorf("du not supported for this build") } // Find will always return zero since is on unsupported platform. func Find(path string) (int64, error) { return 0, fmt.Errorf("find not supported for this build") </del> <ins> func DiskUsage(path string) (UsageInfo, error) { var usage UsageInfo return usage, fmt.Errorf("directory disk usage not supported for this build.") </ins> }

"unsafe" "golang.org/x/sys/windows" <del> "k8s.io/apimachinery/pkg/api/resource" </del> ) var (

procGetDiskFreeSpaceEx = modkernel32.NewProc("GetDiskFreeSpaceExW") ) <ins> type UsageInfo struct { Bytes  int64 Inodes int64 } </ins> // Info returns (available bytes, byte capacity, byte usage, total inodes, inodes free, inode usage, error) // for the filesystem that path resides upon. func Info(path string) (int64, int64, int64, int64, int64, int64, error) {

} // DiskUsage gets disk usage of specified path. <del> func DiskUsage(path string) (*resource.Quantity, error) { </del> <ins> func DiskUsage(path string) (UsageInfo, error) { var usage UsageInfo </ins> info, err := os.Lstat(path) if err != nil { <del> return nil, err } usage, err := diskUsage(path, info) if err != nil { return nil, err </del> <ins> return usage, err </ins> } <del> used, err := resource.ParseQuantity(fmt.Sprintf("%d", usage)) if err != nil { return nil, fmt.Errorf("failed to parse fs usage %d due to %v", usage, err) } used.Format = resource.BinarySI return &used, nil } // Find will always return zero since inodes is not supported on Windows. func Find(path string) (int64, error) { return 0, nil </del> <ins> usage.Bytes, err = diskUsage(path, info) return usage, err </ins> } func diskUsage(currPath string, info os.FileInfo) (int64, error) {

} total := dirInfo1.Size() + dirInfo2.Size() + fileInfo1.Size() + fileInfo2.Size() <del> size, err := DiskUsage(dir1) </del> <ins> usage, err := DiskUsage(dir1) </ins> if err != nil { t.Fatalf("TestDiskUsage failed: %s", err.Error()) } <ins> size, err := resource.ParseQuantity(fmt.Sprintf("%d", usage.Bytes)) if err != nil { t.Fatalf("TestDiskUsage failed: %s", err.Error()) } </ins> used, err := resource.ParseQuantity(fmt.Sprintf("%d", total)) if err != nil { t.Fatalf("TestDiskUsage failed: %s", err.Error())