diff --git a/pkg/controller/persistentvolume/persistentvolume_claim_binder_controller_test.go b/pkg/controller/persistentvolume/persistentvolume_claim_binder_controller_test.go index 87612bc04373e..2f7688cffa92b 100644 --- a/pkg/controller/persistentvolume/persistentvolume_claim_binder_controller_test.go +++ b/pkg/controller/persistentvolume/persistentvolume_claim_binder_controller_test.go @@ -420,6 +420,7 @@ func newMockRecycler(spec *volume.Spec, host volume.VolumeHost, config volume.Vo type mockRecycler struct { path string host volume.VolumeHost + volume.MetricsNil } func (r *mockRecycler) GetPath() string { diff --git a/pkg/kubelet/kubelet_test.go b/pkg/kubelet/kubelet_test.go index 3cfc5131ccd2b..09b71e99fc1c1 100644 --- a/pkg/kubelet/kubelet_test.go +++ b/pkg/kubelet/kubelet_test.go @@ -514,6 +514,7 @@ func TestGetPodVolumesFromDisk(t *testing.T) { type stubVolume struct { path string + volume.MetricsNil } func (f *stubVolume) GetPath() string { @@ -559,9 +560,9 @@ func TestMakeVolumeMounts(t *testing.T) { } podVolumes := kubecontainer.VolumeMap{ - "disk": kubecontainer.VolumeInfo{Builder: &stubVolume{"/mnt/disk"}}, - "disk4": kubecontainer.VolumeInfo{Builder: &stubVolume{"/mnt/host"}}, - "disk5": kubecontainer.VolumeInfo{Builder: &stubVolume{"/var/lib/kubelet/podID/volumes/empty/disk5"}}, + "disk": kubecontainer.VolumeInfo{Builder: &stubVolume{path: "/mnt/disk"}}, + "disk4": kubecontainer.VolumeInfo{Builder: &stubVolume{path: "/mnt/host"}}, + "disk5": kubecontainer.VolumeInfo{Builder: &stubVolume{path: "/var/lib/kubelet/podID/volumes/empty/disk5"}}, } pod := api.Pod{ diff --git a/pkg/volume/aws_ebs/aws_ebs.go b/pkg/volume/aws_ebs/aws_ebs.go index ec3a7f02f7fbe..6034173247a2a 100644 --- a/pkg/volume/aws_ebs/aws_ebs.go +++ b/pkg/volume/aws_ebs/aws_ebs.go @@ -144,6 +144,7 @@ type awsElasticBlockStore struct { // Mounter interface that provides system calls to mount the global path to the pod local path. mounter mount.Interface plugin *awsElasticBlockStorePlugin + volume.MetricsNil } func detachDiskLogError(ebs *awsElasticBlockStore) { diff --git a/pkg/volume/cephfs/cephfs.go b/pkg/volume/cephfs/cephfs.go index 5924da317a14d..926b11399e538 100644 --- a/pkg/volume/cephfs/cephfs.go +++ b/pkg/volume/cephfs/cephfs.go @@ -143,6 +143,7 @@ type cephfs struct { readonly bool mounter mount.Interface plugin *cephfsPlugin + volume.MetricsNil } type cephfsBuilder struct { diff --git a/pkg/volume/cinder/cinder.go b/pkg/volume/cinder/cinder.go index c9f0c397c8b65..f1366a04831ff 100644 --- a/pkg/volume/cinder/cinder.go +++ b/pkg/volume/cinder/cinder.go @@ -144,6 +144,7 @@ type cinderVolume struct { // diskMounter provides the interface that is used to mount the actual block device. blockDeviceMounter mount.Interface plugin *cinderPlugin + volume.MetricsNil } func detachDiskLogError(cd *cinderVolume) { diff --git a/pkg/volume/downwardapi/downwardapi.go b/pkg/volume/downwardapi/downwardapi.go index 953070cef471d..dfbe917d82498 100644 --- a/pkg/volume/downwardapi/downwardapi.go +++ b/pkg/volume/downwardapi/downwardapi.go @@ -90,6 +90,7 @@ type downwardAPIVolume struct { pod *api.Pod podUID types.UID // TODO: remove this redundancy as soon NewCleaner func will have *api.POD and not only types.UID plugin *downwardAPIPlugin + volume.MetricsNil } // This is the spec for the volume that this plugin wraps. diff --git a/pkg/volume/empty_dir/empty_dir.go b/pkg/volume/empty_dir/empty_dir.go index 5c0a99fda7c4c..9defc41531c7f 100644 --- a/pkg/volume/empty_dir/empty_dir.go +++ b/pkg/volume/empty_dir/empty_dir.go @@ -79,13 +79,14 @@ func (plugin *emptyDirPlugin) newBuilderInternal(spec *volume.Spec, pod *api.Pod medium = spec.Volume.EmptyDir.Medium } return &emptyDir{ - pod: pod, - volName: spec.Name(), - medium: medium, - mounter: mounter, - mountDetector: mountDetector, - plugin: plugin, - rootContext: opts.RootContext, + pod: pod, + volName: spec.Name(), + medium: medium, + mounter: mounter, + mountDetector: mountDetector, + plugin: plugin, + rootContext: opts.RootContext, + MetricsProvider: volume.NewMetricsDu(GetPath(pod.UID, spec.Name(), plugin.host)), }, nil } @@ -96,12 +97,13 @@ func (plugin *emptyDirPlugin) NewCleaner(volName string, podUID types.UID) (volu func (plugin *emptyDirPlugin) newCleanerInternal(volName string, podUID types.UID, mounter mount.Interface, mountDetector mountDetector) (volume.Cleaner, error) { ed := &emptyDir{ - pod: &api.Pod{ObjectMeta: api.ObjectMeta{UID: podUID}}, - volName: volName, - medium: api.StorageMediumDefault, // might be changed later - mounter: mounter, - mountDetector: mountDetector, - plugin: plugin, + pod: &api.Pod{ObjectMeta: api.ObjectMeta{UID: podUID}}, + volName: volName, + medium: api.StorageMediumDefault, // might be changed later + mounter: mounter, + mountDetector: mountDetector, + plugin: plugin, + MetricsProvider: volume.NewMetricsDu(GetPath(podUID, volName, plugin.host)), } return ed, nil } @@ -133,6 +135,7 @@ type emptyDir struct { mountDetector mountDetector plugin *emptyDirPlugin rootContext string + volume.MetricsProvider } func (ed *emptyDir) GetAttributes() volume.Attributes { @@ -265,8 +268,12 @@ func (ed *emptyDir) setupDir(dir string) error { } func (ed *emptyDir) GetPath() string { + return GetPath(ed.pod.UID, ed.volName, ed.plugin.host) +} + +func GetPath(uid types.UID, volName string, host volume.VolumeHost) string { name := emptyDirPluginName - return ed.plugin.host.GetPodVolumeDir(ed.pod.UID, util.EscapeQualifiedNameForDisk(name), ed.volName) + return host.GetPodVolumeDir(uid, util.EscapeQualifiedNameForDisk(name), volName) } // TearDown simply discards everything in the directory. diff --git a/pkg/volume/empty_dir/empty_dir_test.go b/pkg/volume/empty_dir/empty_dir_test.go index e28ff9339c87a..738d9cb55b6ff 100644 --- a/pkg/volume/empty_dir/empty_dir_test.go +++ b/pkg/volume/empty_dir/empty_dir_test.go @@ -274,3 +274,42 @@ func TestPluginBackCompat(t *testing.T) { t.Errorf("Got unexpected path: %s", volPath) } } + +// TestMetrics tests that MetricProvider methods return sane values. +func TestMetrics(t *testing.T) { + // Create an empty temp directory for the volume + tmpDir, err := ioutil.TempDir(os.TempDir(), "empty_dir_test") + if err != nil { + t.Fatalf("Can't make a tmp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + plug := makePluginUnderTest(t, "kubernetes.io/empty-dir", tmpDir) + + spec := &api.Volume{ + Name: "vol1", + } + pod := &api.Pod{ObjectMeta: api.ObjectMeta{UID: types.UID("poduid")}} + builder, err := plug.NewBuilder(volume.NewSpecFromVolume(spec), pod, volume.VolumeOptions{RootContext: ""}) + if err != nil { + t.Errorf("Failed to make a new Builder: %v", err) + } + + // Need to create the subdirectory + os.MkdirAll(builder.GetPath(), 0755) + + // TODO(pwittroc): Move this into a reusable testing utility + metrics, err := builder.GetMetrics() + if err != nil { + t.Errorf("Unexpected error when calling GetMetrics %v", err) + } + if metrics.Used.Value() != 4096 { + t.Errorf("Expected Used %d to be 4096", metrics.Used.Value()) + } + if metrics.Capacity.Value() <= 0 { + t.Errorf("Expected Capacity to be greater than 0") + } + if metrics.Available.Value() <= 0 { + t.Errorf("Expected Available to be greater than 0") + } +} diff --git a/pkg/volume/fc/fc.go b/pkg/volume/fc/fc.go index 76b69fe5f8c0f..0b208d268dd5f 100644 --- a/pkg/volume/fc/fc.go +++ b/pkg/volume/fc/fc.go @@ -149,6 +149,7 @@ type fcDisk struct { manager diskManager // io handler interface io ioHandler + volume.MetricsNil } func (fc *fcDisk) GetPath() string { diff --git a/pkg/volume/flocker/plugin.go b/pkg/volume/flocker/plugin.go index 4d26555bc631b..d2c99265c7b3b 100644 --- a/pkg/volume/flocker/plugin.go +++ b/pkg/volume/flocker/plugin.go @@ -111,6 +111,7 @@ type flockerBuilder struct { exe exec.Interface opts volume.VolumeOptions readOnly bool + volume.MetricsNil } func (b flockerBuilder) GetAttributes() volume.Attributes { diff --git a/pkg/volume/gce_pd/gce_pd.go b/pkg/volume/gce_pd/gce_pd.go index 5624835fe2f17..7cf0cce6b8806 100644 --- a/pkg/volume/gce_pd/gce_pd.go +++ b/pkg/volume/gce_pd/gce_pd.go @@ -144,6 +144,7 @@ type gcePersistentDisk struct { // Mounter interface that provides system calls to mount the global path to the pod local path. mounter mount.Interface plugin *gcePersistentDiskPlugin + volume.MetricsNil } func detachDiskLogError(pd *gcePersistentDisk) { diff --git a/pkg/volume/git_repo/git_repo.go b/pkg/volume/git_repo/git_repo.go index c927e77738a23..4c959cc2ccb94 100644 --- a/pkg/volume/git_repo/git_repo.go +++ b/pkg/volume/git_repo/git_repo.go @@ -89,6 +89,7 @@ type gitRepoVolume struct { volName string podUID types.UID plugin *gitRepoPlugin + volume.MetricsNil } var _ volume.Volume = &gitRepoVolume{} diff --git a/pkg/volume/glusterfs/glusterfs.go b/pkg/volume/glusterfs/glusterfs.go index 6869593b65465..0e9179fb9834c 100644 --- a/pkg/volume/glusterfs/glusterfs.go +++ b/pkg/volume/glusterfs/glusterfs.go @@ -142,6 +142,7 @@ type glusterfs struct { pod *api.Pod mounter mount.Interface plugin *glusterfsPlugin + volume.MetricsNil } type glusterfsBuilder struct { diff --git a/pkg/volume/host_path/host_path.go b/pkg/volume/host_path/host_path.go index 8c8d222250ffd..526bac2f0e518 100644 --- a/pkg/volume/host_path/host_path.go +++ b/pkg/volume/host_path/host_path.go @@ -95,20 +95,25 @@ func (plugin *hostPathPlugin) GetAccessModes() []api.PersistentVolumeAccessMode func (plugin *hostPathPlugin) NewBuilder(spec *volume.Spec, pod *api.Pod, _ volume.VolumeOptions) (volume.Builder, error) { if spec.Volume != nil && spec.Volume.HostPath != nil { + path := spec.Volume.HostPath.Path return &hostPathBuilder{ - hostPath: &hostPath{path: spec.Volume.HostPath.Path}, + hostPath: &hostPath{path: path, MetricsProvider: volume.NewMetricsDu(path)}, readOnly: false, }, nil } else { + path := spec.PersistentVolume.Spec.HostPath.Path return &hostPathBuilder{ - hostPath: &hostPath{path: spec.PersistentVolume.Spec.HostPath.Path}, + hostPath: &hostPath{path: path, MetricsProvider: volume.NewMetricsDu(path)}, readOnly: spec.ReadOnly, }, nil } } func (plugin *hostPathPlugin) NewCleaner(volName string, podUID types.UID) (volume.Cleaner, error) { - return &hostPathCleaner{&hostPath{""}}, nil + return &hostPathCleaner{&hostPath{ + path: "", + MetricsProvider: volume.NewMetricsDu(""), + }}, nil } func (plugin *hostPathPlugin) NewRecycler(spec *volume.Spec) (volume.Recycler, error) { @@ -130,12 +135,14 @@ func newRecycler(spec *volume.Spec, host volume.VolumeHost, config volume.Volume if spec.PersistentVolume == nil || spec.PersistentVolume.Spec.HostPath == nil { return nil, fmt.Errorf("spec.PersistentVolumeSource.HostPath is nil") } + path := spec.PersistentVolume.Spec.HostPath.Path return &hostPathRecycler{ - name: spec.Name(), - path: spec.PersistentVolume.Spec.HostPath.Path, - host: host, - config: config, - timeout: volume.CalculateTimeoutForVolume(config.RecyclerMinimumTimeout, config.RecyclerTimeoutIncrement, spec.PersistentVolume), + name: spec.Name(), + path: path, + host: host, + config: config, + timeout: volume.CalculateTimeoutForVolume(config.RecyclerMinimumTimeout, config.RecyclerTimeoutIncrement, spec.PersistentVolume), + MetricsProvider: volume.NewMetricsDu(path), }, nil } @@ -143,7 +150,8 @@ func newDeleter(spec *volume.Spec, host volume.VolumeHost) (volume.Deleter, erro if spec.PersistentVolume != nil && spec.PersistentVolume.Spec.HostPath == nil { return nil, fmt.Errorf("spec.PersistentVolumeSource.HostPath is nil") } - return &hostPathDeleter{spec.Name(), spec.PersistentVolume.Spec.HostPath.Path, host}, nil + path := spec.PersistentVolume.Spec.HostPath.Path + return &hostPathDeleter{spec.Name(), path, host, volume.NewMetricsDu(path)}, nil } func newCreater(options volume.VolumeOptions, host volume.VolumeHost) (volume.Creater, error) { @@ -154,6 +162,7 @@ func newCreater(options volume.VolumeOptions, host volume.VolumeHost) (volume.Cr // The direct at the specified path will be directly exposed to the container. type hostPath struct { path string + volume.MetricsProvider } func (hp *hostPath) GetPath() string { @@ -214,6 +223,7 @@ type hostPathRecycler struct { host volume.VolumeHost config volume.VolumeConfig timeout int64 + volume.MetricsProvider } func (r *hostPathRecycler) GetPath() string { @@ -280,6 +290,7 @@ type hostPathDeleter struct { name string path string host volume.VolumeHost + volume.MetricsProvider } func (r *hostPathDeleter) GetPath() string { diff --git a/pkg/volume/host_path/host_path_test.go b/pkg/volume/host_path/host_path_test.go index 0ec026346c597..f5681ea9a3fc0 100644 --- a/pkg/volume/host_path/host_path_test.go +++ b/pkg/volume/host_path/host_path_test.go @@ -18,6 +18,7 @@ package host_path import ( "fmt" + "io/ioutil" "os" "testing" @@ -92,7 +93,7 @@ func TestDeleter(t *testing.T) { defer os.RemoveAll(tempPath) err := os.MkdirAll(tempPath, 0750) if err != nil { - t.Fatal("Failed to create tmp directory for deleter: %v", err) + t.Fatalf("Failed to create tmp directory for deleter: %v", err) } plugMgr := volume.VolumePluginMgr{} @@ -272,3 +273,45 @@ func TestPersistentClaimReadOnlyFlag(t *testing.T) { t.Errorf("Expected true for builder.IsReadOnly") } } + +// TestMetrics tests that MetricProvider methods return sane values. +func TestMetrics(t *testing.T) { + // Create an empty temp directory for the volume + tmpDir, err := ioutil.TempDir(os.TempDir(), "host_path_test") + if err != nil { + t.Fatalf("Can't make a tmp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + plugMgr := volume.VolumePluginMgr{} + plugMgr.InitPlugins(ProbeVolumePlugins(volume.VolumeConfig{}), volume.NewFakeVolumeHost(tmpDir, nil, nil)) + + plug, err := plugMgr.FindPluginByName("kubernetes.io/host-path") + if err != nil { + t.Errorf("Can't find the plugin by name") + } + spec := &api.Volume{ + Name: "vol1", + VolumeSource: api.VolumeSource{HostPath: &api.HostPathVolumeSource{Path: tmpDir}}, + } + pod := &api.Pod{ObjectMeta: api.ObjectMeta{UID: types.UID("poduid")}} + builder, err := plug.NewBuilder(volume.NewSpecFromVolume(spec), pod, volume.VolumeOptions{}) + if err != nil { + t.Errorf("Failed to make a new Builder: %v", err) + } + + // TODO(pwittroc): Move this into a reusable testing utility + metrics, err := builder.GetMetrics() + if err != nil { + t.Errorf("Unexpected error when calling GetMetrics %v", err) + } + if metrics.Used.Value() != 4096 { + t.Errorf("Expected Used %d to be 4096", metrics.Used) + } + if metrics.Capacity.Value() <= 0 { + t.Errorf("Expected Capacity to be greater than 0") + } + if metrics.Available.Value() <= 0 { + t.Errorf("Expected Available to be greater than 0") + } +} diff --git a/pkg/volume/iscsi/iscsi.go b/pkg/volume/iscsi/iscsi.go index a630cb9125667..644c5619da29c 100644 --- a/pkg/volume/iscsi/iscsi.go +++ b/pkg/volume/iscsi/iscsi.go @@ -147,6 +147,7 @@ type iscsiDisk struct { plugin *iscsiPlugin // Utility interface that provides API calls to the provider to attach/detach disks. manager diskManager + volume.MetricsNil } func (iscsi *iscsiDisk) GetPath() string { diff --git a/pkg/volume/metrics_du.go b/pkg/volume/metrics_du.go new file mode 100644 index 0000000000000..9defd0143beef --- /dev/null +++ b/pkg/volume/metrics_du.go @@ -0,0 +1,91 @@ +/* +Copyright 2014 The Kubernetes Authors All rights reserved. + +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 ( + "errors" + "fmt" + "os/exec" + "strings" + + "k8s.io/kubernetes/pkg/api/resource" + "k8s.io/kubernetes/pkg/volume/util" +) + +var _ MetricsProvider = &metricsDu{} + +// metricsDu represents a MetricsProvider that calculates the used and available +// Volume space by executing the "du" command and gathering filesystem info for the Volume path. +type metricsDu struct { + // the directory path the volume is mounted to. + path string +} + +// NewMetricsDu creates a new metricsDu with the Volume path. +func NewMetricsDu(path string) MetricsProvider { + return &metricsDu{path} +} + +// See MetricsProvider.GetMetrics +// GetMetrics calculates the volume usage and device free space by executing "du" +// and gathering filesystem info for the Volume path. +func (md *metricsDu) GetMetrics() (*Metrics, error) { + metrics := &Metrics{} + if md.path == "" { + return metrics, errors.New("no path defined for disk usage metrics.") + } + + err := md.runDu(metrics) + if err != nil { + return metrics, err + } + + err = md.getFsInfo(metrics) + if err != nil { + return metrics, err + } + + return metrics, nil +} + +// runDu executes the "du" command and writes the results to metrics.Used +func (md *metricsDu) runDu(metrics *Metrics) error { + // 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", "-s", "-B", "1", md.path).CombinedOutput() + if err != nil { + return fmt.Errorf("failed command 'du' on %s with error %v", md.path, err) + } + used, err := resource.ParseQuantity(strings.Fields(string(out))[0]) + if err != nil { + return fmt.Errorf("failed to parse 'du' output %s due to error %v", out, err) + } + used.Format = resource.BinarySI + metrics.Used = used + return nil +} + +// getFsInfo writes metrics.Capacity and metrics.Available from the filesystem info +func (md *metricsDu) getFsInfo(metrics *Metrics) error { + available, capacity, err := util.FsInfo(md.path) + if err != nil { + return fmt.Errorf("Failed to get FsInfo due to error %v", err) + } + metrics.Available = resource.NewQuantity(available, resource.BinarySI) + metrics.Capacity = resource.NewQuantity(capacity, resource.BinarySI) + return nil +} diff --git a/pkg/volume/metrics_du_test.go b/pkg/volume/metrics_du_test.go new file mode 100644 index 0000000000000..69d124b1bf431 --- /dev/null +++ b/pkg/volume/metrics_du_test.go @@ -0,0 +1,90 @@ +/* +Copyright 2015 The Kubernetes Authors All rights reserved. + +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 ( + "io/ioutil" + "os" + "path/filepath" + "testing" +) + +// TestMetricsDuGetCapacity tests that MetricsDu can read disk usage +// for path +func TestMetricsDuGetCapacity(t *testing.T) { + tmpDir, err := ioutil.TempDir(os.TempDir(), "metrics_du_test") + if err != nil { + t.Fatalf("Can't make a tmp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + metrics := NewMetricsDu(tmpDir) + + actual, err := metrics.GetMetrics() + if err != nil { + t.Errorf("Unexpected error when calling GetMetrics %v", err) + } + if actual.Used.Value() != 4096 { + t.Errorf("Expected Used %d for empty directory to be 4096.", actual.Used.Value()) + } + + // TODO(pwittroc): Figure out a way to test these values for correctness, maybe by formatting and mounting a file + // as a filesystem + if actual.Capacity.Value() <= 0 { + t.Errorf("Expected Capacity %d to be greater than 0.", actual.Capacity.Value()) + } + if actual.Available.Value() <= 0 { + t.Errorf("Expected Available %d to be greater than 0.", actual.Available.Value()) + } + + // Write a file and expect Used to increase + ioutil.WriteFile(filepath.Join(tmpDir, "f1"), []byte("Hello World"), os.ModeTemporary) + actual, err = metrics.GetMetrics() + if err != nil { + t.Errorf("Unexpected error when calling GetMetrics %v", err) + } + if actual.Used.Value() != 8192 { + t.Errorf("Unexpected Used for directory with file. Expected 8192, was %d.", actual.Used.Value()) + } +} + +// TestMetricsDuRequireInit tests that if MetricsDu is not initialized with a path, GetMetrics +// returns an error +func TestMetricsDuRequirePath(t *testing.T) { + metrics := &metricsDu{} + actual, err := metrics.GetMetrics() + expected := &Metrics{} + if *actual != *expected { + t.Errorf("Expected empty Metrics from uninitialized MetricsDu, actual %v", *actual) + } + if err == nil { + t.Errorf("Expected error when calling GetMetrics on uninitialized MetricsDu, actual nil") + } +} + +// TestMetricsDuRealDirectory tests that if MetricsDu is initialized to a non-existent path, GetMetrics +// returns an error +func TestMetricsDuRequireRealDirectory(t *testing.T) { + metrics := NewMetricsDu("/not/a/real/directory") + actual, err := metrics.GetMetrics() + expected := &Metrics{} + if *actual != *expected { + t.Errorf("Expected empty Metrics from incorrectly initialized MetricsDu, actual %v", *actual) + } + if err == nil { + t.Errorf("Expected error when calling GetMetrics on incorrectly initialized MetricsDu, actual nil") + } +} diff --git a/pkg/volume/metrics_nil.go b/pkg/volume/metrics_nil.go new file mode 100644 index 0000000000000..c7fffdb88acf7 --- /dev/null +++ b/pkg/volume/metrics_nil.go @@ -0,0 +1,31 @@ +/* +Copyright 2014 The Kubernetes Authors All rights reserved. + +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 "errors" + +var _ MetricsProvider = &MetricsNil{} + +// MetricsNil represents a MetricsProvider that does not support returning +// Metrics. It serves as a placeholder for Volumes that do not yet support metrics. +type MetricsNil struct{} + +// See MetricsProvider.GetMetrics +// GetMetrics returns an empty Metrics and an error. +func (*MetricsNil) GetMetrics() (*Metrics, error) { + return &Metrics{}, errors.New("metrics are not supported for MetricsNil Volumes") +} diff --git a/pkg/volume/metrics_nil_test.go b/pkg/volume/metrics_nil_test.go new file mode 100644 index 0000000000000..9f5bb61ae1a28 --- /dev/null +++ b/pkg/volume/metrics_nil_test.go @@ -0,0 +1,33 @@ +/* +Copyright 2015 The Kubernetes Authors All rights reserved. + +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 ( + "testing" +) + +func TestMetricsNilGetCapacity(t *testing.T) { + metrics := &MetricsNil{} + actual, err := metrics.GetMetrics() + expected := &Metrics{} + if *actual != *expected { + t.Errorf("Expected empty Metrics, actual %v", *actual) + } + if err == nil { + t.Errorf("Expected error when calling GetMetrics, actual nil") + } +} diff --git a/pkg/volume/nfs/nfs.go b/pkg/volume/nfs/nfs.go index c4027aeb5f1f1..a606e0447199b 100644 --- a/pkg/volume/nfs/nfs.go +++ b/pkg/volume/nfs/nfs.go @@ -131,6 +131,7 @@ type nfs struct { plugin *nfsPlugin // decouple creating recyclers by deferring to a function. Allows for easier testing. newRecyclerFunc func(spec *volume.Spec, host volume.VolumeHost, volumeConfig volume.VolumeConfig) (volume.Recycler, error) + volume.MetricsNil } func (nfsVolume *nfs) GetPath() string { @@ -271,6 +272,7 @@ type nfsRecycler struct { host volume.VolumeHost config volume.VolumeConfig timeout int64 + volume.MetricsNil } func (r *nfsRecycler) GetPath() string { diff --git a/pkg/volume/nfs/nfs_test.go b/pkg/volume/nfs/nfs_test.go index 97e341b01050c..8dd1414c44c2d 100644 --- a/pkg/volume/nfs/nfs_test.go +++ b/pkg/volume/nfs/nfs_test.go @@ -92,6 +92,7 @@ func newMockRecycler(spec *volume.Spec, host volume.VolumeHost, config volume.Vo type mockRecycler struct { path string host volume.VolumeHost + volume.MetricsNil } func (r *mockRecycler) GetPath() string { diff --git a/pkg/volume/rbd/rbd.go b/pkg/volume/rbd/rbd.go index 78d628b291972..54679a841c83f 100644 --- a/pkg/volume/rbd/rbd.go +++ b/pkg/volume/rbd/rbd.go @@ -172,6 +172,7 @@ type rbd struct { mounter *mount.SafeFormatAndMount // Utility interface that provides API calls to the provider to attach/detach disks. manager diskManager + volume.MetricsNil } func (rbd *rbd) GetPath() string { diff --git a/pkg/volume/secret/secret.go b/pkg/volume/secret/secret.go index 607abb7ae8df1..246256acfa73c 100644 --- a/pkg/volume/secret/secret.go +++ b/pkg/volume/secret/secret.go @@ -61,14 +61,14 @@ func (plugin *secretPlugin) CanSupport(spec *volume.Spec) bool { func (plugin *secretPlugin) NewBuilder(spec *volume.Spec, pod *api.Pod, opts volume.VolumeOptions) (volume.Builder, error) { return &secretVolumeBuilder{ - secretVolume: &secretVolume{spec.Name(), pod.UID, plugin, plugin.host.GetMounter(), plugin.host.GetWriter()}, + secretVolume: &secretVolume{spec.Name(), pod.UID, plugin, plugin.host.GetMounter(), plugin.host.GetWriter(), volume.MetricsNil{}}, secretName: spec.Volume.Secret.SecretName, pod: *pod, opts: &opts}, nil } func (plugin *secretPlugin) NewCleaner(volName string, podUID types.UID) (volume.Cleaner, error) { - return &secretVolumeCleaner{&secretVolume{volName, podUID, plugin, plugin.host.GetMounter(), plugin.host.GetWriter()}}, nil + return &secretVolumeCleaner{&secretVolume{volName, podUID, plugin, plugin.host.GetMounter(), plugin.host.GetWriter(), volume.MetricsNil{}}}, nil } type secretVolume struct { @@ -77,6 +77,7 @@ type secretVolume struct { plugin *secretPlugin mounter mount.Interface writer ioutil.Writer + volume.MetricsNil } var _ volume.Volume = &secretVolume{} diff --git a/pkg/volume/testing.go b/pkg/volume/testing.go index c6b92fe87fad8..267a5a1838cb5 100644 --- a/pkg/volume/testing.go +++ b/pkg/volume/testing.go @@ -136,19 +136,19 @@ func (plugin *FakeVolumePlugin) CanSupport(spec *Spec) bool { } func (plugin *FakeVolumePlugin) NewBuilder(spec *Spec, pod *api.Pod, opts VolumeOptions) (Builder, error) { - return &FakeVolume{pod.UID, spec.Name(), plugin}, nil + return &FakeVolume{pod.UID, spec.Name(), plugin, MetricsNil{}}, nil } func (plugin *FakeVolumePlugin) NewCleaner(volName string, podUID types.UID) (Cleaner, error) { - return &FakeVolume{podUID, volName, plugin}, nil + return &FakeVolume{podUID, volName, plugin, MetricsNil{}}, nil } func (plugin *FakeVolumePlugin) NewRecycler(spec *Spec) (Recycler, error) { - return &fakeRecycler{"/attributesTransferredFromSpec"}, nil + return &fakeRecycler{"/attributesTransferredFromSpec", MetricsNil{}}, nil } func (plugin *FakeVolumePlugin) NewDeleter(spec *Spec) (Deleter, error) { - return &FakeDeleter{"/attributesTransferredFromSpec"}, nil + return &FakeDeleter{"/attributesTransferredFromSpec", MetricsNil{}}, nil } func (plugin *FakeVolumePlugin) GetAccessModes() []api.PersistentVolumeAccessMode { @@ -159,6 +159,7 @@ type FakeVolume struct { PodUID types.UID VolName string Plugin *FakeVolumePlugin + MetricsNil } func (_ *FakeVolume) GetAttributes() Attributes { @@ -192,6 +193,7 @@ func (fv *FakeVolume) TearDownAt(dir string) error { type fakeRecycler struct { path string + MetricsNil } func (fr *fakeRecycler) Recycle() error { @@ -214,6 +216,7 @@ func NewFakeRecycler(spec *Spec, host VolumeHost, config VolumeConfig) (Recycler type FakeDeleter struct { path string + MetricsNil } func (fd *FakeDeleter) Delete() error { diff --git a/pkg/volume/util/fs_linux.go b/pkg/volume/util/fs_linux.go new file mode 100644 index 0000000000000..3713eaa686947 --- /dev/null +++ b/pkg/volume/util/fs_linux.go @@ -0,0 +1,41 @@ +// +build linux + +/* +Copyright 2014 The Kubernetes Authors All rights reserved. + +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 util + +import ( + "syscall" +) + +// FSInfo linux returns (available bytes, byte capacity, error) for the filesystem that +// path resides upon. +func FsInfo(path string) (int64, int64, error) { + statfs := &syscall.Statfs_t{} + err := syscall.Statfs(path, statfs) + if err != nil { + return 0, 0, err + } + + // Available is blocks available * fragment size + available := int64(statfs.Bavail) * int64(statfs.Frsize) + + // Capacity is total block count * fragment size + capacity := int64(statfs.Blocks) * int64(statfs.Frsize) + + return available, capacity, nil +} diff --git a/pkg/volume/util/fs_unsupported.go b/pkg/volume/util/fs_unsupported.go new file mode 100644 index 0000000000000..3b63bc7134296 --- /dev/null +++ b/pkg/volume/util/fs_unsupported.go @@ -0,0 +1,28 @@ +// +build !linux + +/* +Copyright 2014 The Kubernetes Authors All rights reserved. + +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 util + +import ( + "errors" +) + +// FSInfo unsupported returns 0 values for available and capacity and an error. +func FsInfo(path string) (int64, int64, error) { + return 0, 0, errors.New("FsInfo not supported for this build.") +} diff --git a/pkg/volume/volume.go b/pkg/volume/volume.go index 4121a09bcc963..ee9d213aae8af 100644 --- a/pkg/volume/volume.go +++ b/pkg/volume/volume.go @@ -19,6 +19,7 @@ package volume import ( "io/ioutil" "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/resource" "os" "path" ) @@ -28,6 +29,32 @@ import ( type Volume interface { // GetPath returns the directory path the volume is mounted to. GetPath() string + + // MetricsProvider embeds methods for exposing metrics (e.g. used,available space). + MetricsProvider +} + +// MetricsProvider exposes metrics (e.g. used,available space) related to a Volume. +type MetricsProvider interface { + // GetMetrics returns the Metrics for the Volume. Maybe expensive for some implementations. + GetMetrics() (*Metrics, error) +} + +// Metrics represents the used and available bytes of the Volume. +type Metrics struct { + // Used represents the total bytes used by the Volume. + // Note: For block devices this maybe more than the total size of the files. + Used *resource.Quantity + + // Capacity represents the total capacity (bytes) of the volume's underlying storage. + // For Volumes that share a filesystem with the host (e.g. emptydir, hostpath) this is the size + // of the underlying storage, and will not equal Used + Available as the fs is shared. + Capacity *resource.Quantity + + // Available represents the storage space available (bytes) for the Volume. + // For Volumes that share a filesystem with the host (e.g. emptydir, hostpath), this is the available + // space on the underlying storage, and is shared with host processes and other Volumes. + Available *resource.Quantity } // Attributes represents the attributes of this builder.