|  | Copyright 2020 The Monogon Project Authors. | 
|  |  | 
|  | Licensed under the Apache License, Version 2.0 (the "License"); | 
|  | you may not use this file except in compliance with the License. | 
|  | You may obtain a copy of the License at | 
|  |  | 
|  | http://www.apache.org/licenses/LICENSE-2.0 | 
|  |  | 
|  | Unless required by applicable law or agreed to in writing, software | 
|  | distributed under the License is distributed on an "AS IS" BASIS, | 
|  | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
|  | See the License for the specific language governing permissions and | 
|  | limitations under the License. | 
|  |  | 
|  |  | 
|  | From b16e57cc52a437465bbd12c24fb05fe5790afe1d Mon Sep 17 00:00:00 2001 | 
|  | From: Lorenz Brun <lorenz@brun.one> | 
|  | Date: Tue, 17 Mar 2020 21:41:08 +0100 | 
|  | Subject: [PATCH 2/3] Add a native volume metrics implementation | 
|  |  | 
|  | --- | 
|  | pkg/volume/BUILD                  |   3 + | 
|  | pkg/volume/metrics_native.go      | 101 +++++++++++++++++++++++++++++ | 
|  | pkg/volume/metrics_native_test.go | 102 ++++++++++++++++++++++++++++++ | 
|  | 3 files changed, 206 insertions(+) | 
|  | create mode 100644 pkg/volume/metrics_native.go | 
|  | create mode 100644 pkg/volume/metrics_native_test.go | 
|  |  | 
|  | diff --git a/pkg/volume/BUILD b/pkg/volume/BUILD | 
|  | index 720b13406dc..b6e4b7e6d6f 100644 | 
|  | --- a/pkg/volume/BUILD | 
|  | +++ b/pkg/volume/BUILD | 
|  | @@ -7,6 +7,7 @@ go_library( | 
|  | "metrics_cached.go", | 
|  | "metrics_du.go", | 
|  | "metrics_errors.go", | 
|  | +        "metrics_native.go", | 
|  | "metrics_nil.go", | 
|  | "metrics_statfs.go", | 
|  | "noop_expandable_plugin.go", | 
|  | @@ -35,6 +36,7 @@ go_library( | 
|  | "@io_k8s_client_go//tools/cache:go_default_library", | 
|  | "@io_k8s_client_go//tools/record:go_default_library", | 
|  | "@io_k8s_cloud_provider//:go_default_library", | 
|  | +        "@org_golang_x_sys//unix:go_default_library", | 
|  | "@io_k8s_klog_v2//:go_default_library", | 
|  | "@io_k8s_utils//exec:go_default_library", | 
|  | "@io_k8s_utils//mount:go_default_library", | 
|  | @@ -55,6 +57,7 @@ go_test( | 
|  | name = "go_default_test", | 
|  | srcs = [ | 
|  | "metrics_du_test.go", | 
|  | +        "metrics_native_test.go", | 
|  | "metrics_nil_test.go", | 
|  | "metrics_statfs_test.go", | 
|  | "plugins_test.go", | 
|  | diff --git a/pkg/volume/metrics_native.go b/pkg/volume/metrics_native.go | 
|  | new file mode 100644 | 
|  | index 00000000000..3934b946f2e | 
|  | --- /dev/null | 
|  | +++ b/pkg/volume/metrics_native.go | 
|  | @@ -0,0 +1,101 @@ | 
|  | +/* | 
|  | +Copyright 2020 The Kubernetes Authors. | 
|  | + | 
|  | +Licensed under the Apache License, Version 2.0 (the "License"); | 
|  | +you may not use this file except in compliance with the License. | 
|  | +You may obtain a copy of the License at | 
|  | + | 
|  | +    http://www.apache.org/licenses/LICENSE-2.0 | 
|  | + | 
|  | +Unless required by applicable law or agreed to in writing, software | 
|  | +distributed under the License is distributed on an "AS IS" BASIS, | 
|  | +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
|  | +See the License for the specific language governing permissions and | 
|  | +limitations under the License. | 
|  | +*/ | 
|  | + | 
|  | +package volume | 
|  | + | 
|  | +import ( | 
|  | +	"os" | 
|  | +	"path/filepath" | 
|  | + | 
|  | +	"golang.org/x/sys/unix" | 
|  | +	"k8s.io/apimachinery/pkg/api/resource" | 
|  | +	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | 
|  | +) | 
|  | + | 
|  | +var _ MetricsProvider = &metricsNative{} | 
|  | + | 
|  | +// MetricsNative represents a MetricsProvider that calculates the volume metrics with either a quota | 
|  | +// or by walking the path using syscalls. | 
|  | +type metricsNative struct { | 
|  | +	path string | 
|  | +} | 
|  | + | 
|  | +// NewMetricsNative returns a new metricsNative provider | 
|  | +func NewMetricsNative(path string) MetricsProvider { | 
|  | +	return &metricsNative{path} | 
|  | +} | 
|  | + | 
|  | +// GetMetrics returns an empty Metrics and an error. | 
|  | +// See MetricsProvider.GetMetrics | 
|  | +func (m *metricsNative) GetMetrics() (*Metrics, error) { | 
|  | +	var inodesCount int64 | 
|  | +	var bytesCount int64 | 
|  | + | 
|  | +	var getMetricsRecursive func(path string) error | 
|  | +	getMetricsRecursive = func(path string) error { | 
|  | +		var stat unix.Stat_t | 
|  | +		err := unix.Lstat(path, &stat) | 
|  | +		if os.IsNotExist(err) { | 
|  | +			return nil | 
|  | +		} else if err != nil { | 
|  | +			return err | 
|  | +		} | 
|  | +		// TODO: This double-counts hardlinks | 
|  | +		bytesCount += stat.Blocks * 512 | 
|  | +		inodesCount++ | 
|  | +		if stat.Mode&unix.S_IFDIR != 0 && stat.Mode&unix.S_IFLNK == 0 { | 
|  | +			fd, err := os.Open(path) | 
|  | +			if os.IsNotExist(err) { | 
|  | +				return nil | 
|  | +			} else if err != nil { | 
|  | +				return err | 
|  | +			} | 
|  | +			// We manually close fd before recursing, otherwise we have too many FDs open | 
|  | + | 
|  | +			entries, err := fd.Readdirnames(0) | 
|  | +			if err != nil { | 
|  | +				fd.Close() | 
|  | +				return err | 
|  | +			} | 
|  | +			fd.Close() | 
|  | +			for _, entry := range entries { | 
|  | +				if err := getMetricsRecursive(filepath.Join(path, entry)); err != nil { | 
|  | +					return err | 
|  | +				} | 
|  | +			} | 
|  | +		} | 
|  | +		return nil | 
|  | +	} | 
|  | + | 
|  | +	if err := getMetricsRecursive(m.path); err != nil { | 
|  | +		return &Metrics{}, err | 
|  | +	} | 
|  | + | 
|  | +	var statfs unix.Statfs_t | 
|  | +	if err := unix.Statfs(m.path, &statfs); err != nil { | 
|  | +		return &Metrics{}, err | 
|  | +	} | 
|  | + | 
|  | +	return &Metrics{ | 
|  | +		Time:       metav1.Now(), | 
|  | +		Used:       resource.NewQuantity(bytesCount, resource.BinarySI), | 
|  | +		InodesUsed: resource.NewQuantity(inodesCount, resource.BinarySI), | 
|  | +		Available:  resource.NewQuantity(int64(statfs.Bavail)*statfs.Bsize, resource.BinarySI), | 
|  | +		Capacity:   resource.NewQuantity(int64(statfs.Blocks)*statfs.Bsize, resource.BinarySI), | 
|  | +		Inodes:     resource.NewQuantity(int64(statfs.Files)*statfs.Bsize, resource.BinarySI), | 
|  | +		InodesFree: resource.NewQuantity(int64(statfs.Ffree)*statfs.Bsize, resource.BinarySI), | 
|  | +	}, nil | 
|  | +} | 
|  | diff --git a/pkg/volume/metrics_native_test.go b/pkg/volume/metrics_native_test.go | 
|  | new file mode 100644 | 
|  | index 00000000000..2d5546591ce | 
|  | --- /dev/null | 
|  | +++ b/pkg/volume/metrics_native_test.go | 
|  | @@ -0,0 +1,102 @@ | 
|  | +// +build linux | 
|  | + | 
|  | +/* | 
|  | +Copyright 2015 The Kubernetes Authors. | 
|  | + | 
|  | +Licensed under the Apache License, Version 2.0 (the "License"); | 
|  | +you may not use this file except in compliance with the License. | 
|  | +You may obtain a copy of the License at | 
|  | + | 
|  | +    http://www.apache.org/licenses/LICENSE-2.0 | 
|  | + | 
|  | +Unless required by applicable law or agreed to in writing, software | 
|  | +distributed under the License is distributed on an "AS IS" BASIS, | 
|  | +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
|  | +See the License for the specific language governing permissions and | 
|  | +limitations under the License. | 
|  | +*/ | 
|  | + | 
|  | +package volume_test | 
|  | + | 
|  | +import ( | 
|  | +	"io/ioutil" | 
|  | +	"os" | 
|  | +	"path/filepath" | 
|  | +	"testing" | 
|  | + | 
|  | +	utiltesting "k8s.io/client-go/util/testing" | 
|  | +	. "k8s.io/kubernetes/pkg/volume" | 
|  | +	volumetest "k8s.io/kubernetes/pkg/volume/testing" | 
|  | +) | 
|  | + | 
|  | +// TestMetricsNativeGetCapacity tests that MetricsNative can read disk usage | 
|  | +// for path | 
|  | +func TestMetricsNativeGetCapacity(t *testing.T) { | 
|  | +	tmpDir, err := utiltesting.MkTmpdir("metrics_du_test") | 
|  | +	if err != nil { | 
|  | +		t.Fatalf("Can't make a tmp dir: %v", err) | 
|  | +	} | 
|  | +	defer os.RemoveAll(tmpDir) | 
|  | +	metrics := NewMetricsNative(tmpDir) | 
|  | + | 
|  | +	expectedEmptyDirUsage, err := volumetest.FindEmptyDirectoryUsageOnTmpfs() | 
|  | +	if err != nil { | 
|  | +		t.Errorf("Unexpected error finding expected empty directory usage on tmpfs: %v", err) | 
|  | +	} | 
|  | + | 
|  | +	actual, err := metrics.GetMetrics() | 
|  | +	if err != nil { | 
|  | +		t.Errorf("Unexpected error when calling GetMetrics %v", err) | 
|  | +	} | 
|  | +	if e, a := expectedEmptyDirUsage.Value(), actual.Used.Value(); e != a { | 
|  | +		t.Errorf("Unexpected value for empty directory; expected %v, got %v", e, a) | 
|  | +	} | 
|  | + | 
|  | +	// TODO(pwittroc): Figure out a way to test these values for correctness, maybe by formatting and mounting a file | 
|  | +	// as a filesystem | 
|  | +	if a := actual.Capacity.Value(); a <= 0 { | 
|  | +		t.Errorf("Expected Capacity %d to be greater than 0.", a) | 
|  | +	} | 
|  | +	if a := actual.Available.Value(); a <= 0 { | 
|  | +		t.Errorf("Expected Available %d to be greater than 0.", a) | 
|  | +	} | 
|  | + | 
|  | +	// Write a file in a directory and expect Used to increase | 
|  | +	os.MkdirAll(filepath.Join(tmpDir, "d1"), 0755) | 
|  | +	ioutil.WriteFile(filepath.Join(tmpDir, "d1", "f1"), []byte("Hello World"), os.ModeTemporary) | 
|  | +	actual, err = metrics.GetMetrics() | 
|  | +	if err != nil { | 
|  | +		t.Errorf("Unexpected error when calling GetMetrics %v", err) | 
|  | +	} | 
|  | +	if e, a := (2*expectedEmptyDirUsage.Value() + getExpectedBlockSize(filepath.Join(tmpDir, "d1", "f1"))), actual.Used.Value(); e != a { | 
|  | +		t.Errorf("Unexpected Used for directory with file.  Expected %v, got %d.", e, a) | 
|  | +	} | 
|  | +} | 
|  | + | 
|  | +// TestMetricsNativeRequireInit tests that if MetricsNative is not initialized with a path, GetMetrics | 
|  | +// returns an error | 
|  | +func TestMetricsNativeRequirePath(t *testing.T) { | 
|  | +	metrics := NewMetricsNative("") | 
|  | +	actual, err := metrics.GetMetrics() | 
|  | +	expected := &Metrics{} | 
|  | +	if !volumetest.MetricsEqualIgnoreTimestamp(actual, expected) { | 
|  | +		t.Errorf("Expected empty Metrics from uninitialized MetricsNative, actual %v", *actual) | 
|  | +	} | 
|  | +	if err == nil { | 
|  | +		t.Errorf("Expected error when calling GetMetrics on uninitialized MetricsNative, actual nil") | 
|  | +	} | 
|  | +} | 
|  | + | 
|  | +// TestMetricsNativeRealDirectory tests that if MetricsNative is initialized to a non-existent path, GetMetrics | 
|  | +// returns an error | 
|  | +func TestMetricsNativeRequireRealDirectory(t *testing.T) { | 
|  | +	metrics := NewMetricsNative("/not/a/real/directory") | 
|  | +	actual, err := metrics.GetMetrics() | 
|  | +	expected := &Metrics{} | 
|  | +	if !volumetest.MetricsEqualIgnoreTimestamp(actual, expected) { | 
|  | +		t.Errorf("Expected empty Metrics from incorrectly initialized MetricsNative, actual %v", *actual) | 
|  | +	} | 
|  | +	if err == nil { | 
|  | +		t.Errorf("Expected error when calling GetMetrics on incorrectly initialized MetricsNative, actual nil") | 
|  | +	} | 
|  | +} | 
|  | -- | 
|  | 2.25.1 | 
|  |  |