diff --git a/pkg/volume/metrics_block.go b/pkg/volume/metrics_block.go new file mode 100644 index 00000000000..e0145ae91af --- /dev/null +++ b/pkg/volume/metrics_block.go @@ -0,0 +1,87 @@ +/* +Copyright 2021 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 ( + "fmt" + "io" + "os" + "runtime" + + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var _ MetricsProvider = &metricsBlock{} + +// metricsBlock represents a MetricsProvider that detects the size of the +// BlockMode Volume. +type metricsBlock struct { + // the device node where the volume is attached to. + device string +} + +// NewMetricsStatfs creates a new metricsBlock with the device node of the +// Volume. +func NewMetricsBlock(device string) MetricsProvider { + return &metricsBlock{device} +} + +// See MetricsProvider.GetMetrics +// GetMetrics detects the size of the BlockMode volume for the device node +// where the Volume is attached. +// +// Note that only the capacity of the device can be detected with standard +// tools. Storage systems may have more information that they can provide by +// going through specialized APIs. +func (mb *metricsBlock) GetMetrics() (*Metrics, error) { + // TODO: Windows does not yet support VolumeMode=Block + if runtime.GOOS == "windows" { + return nil, NewNotImplementedError("Windows does not support Block volumes") + } + + metrics := &Metrics{Time: metav1.Now()} + if mb.device == "" { + return metrics, NewNoPathDefinedError() + } + + err := mb.getBlockInfo(metrics) + if err != nil { + return metrics, err + } + + return metrics, nil +} + +// getBlockInfo fetches metrics.Capacity by opening the device and seeking to +// the end. +func (mb *metricsBlock) getBlockInfo(metrics *Metrics) error { + dev, err := os.Open(mb.device) + if err != nil { + return fmt.Errorf("unable to open device %q: %w", mb.device, err) + } + defer dev.Close() + + end, err := dev.Seek(0, io.SeekEnd) + if err != nil { + return fmt.Errorf("failed to detect size of %q: %w", mb.device, err) + } + + metrics.Capacity = resource.NewQuantity(end, resource.BinarySI) + + return nil +} diff --git a/pkg/volume/metrics_block_test.go b/pkg/volume/metrics_block_test.go new file mode 100644 index 00000000000..bd644422928 --- /dev/null +++ b/pkg/volume/metrics_block_test.go @@ -0,0 +1,98 @@ +/* +Copyright 2021 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/fs" + "os" + "runtime" + "testing" + + . "k8s.io/kubernetes/pkg/volume" + volumetest "k8s.io/kubernetes/pkg/volume/testing" +) + +func TestGetMetricsBlockInvalid(t *testing.T) { + metrics := NewMetricsBlock("") + actual, err := metrics.GetMetrics() + expected := &Metrics{} + if !volumetest.MetricsEqualIgnoreTimestamp(actual, expected) { + t.Errorf("Expected empty Metrics from uninitialized MetricsBlock, actual %v", *actual) + } + if err == nil { + t.Errorf("Expected error when calling GetMetrics on uninitialized MetricsBlock, actual nil") + } + + metrics = NewMetricsBlock("/nonexistent/device/node") + actual, err = metrics.GetMetrics() + if !volumetest.MetricsEqualIgnoreTimestamp(actual, expected) { + t.Errorf("Expected empty Metrics from incorrectly initialized MetricsBlock, actual %v", *actual) + } + if err == nil { + t.Errorf("Expected error when calling GetMetrics on incorrectly initialized MetricsBlock, actual nil") + } +} + +func TestGetMetricsBlock(t *testing.T) { + // FIXME: this test is Linux specific + if runtime.GOOS == "windows" { + t.Skip("Block device detection is Linux specific, no Windows support") + } + + // find a block device + // get all available block devices + // - ls /sys/block + devices, err := os.ReadDir("/dev") + if err != nil { + t.Skipf("Could not read devices from /dev: %v", err) + } else if len(devices) == 0 { + t.Skip("No devices found") + } + + // for each device, check if it is available in /dev + devNode := "" + var stat fs.FileInfo + for _, device := range devices { + // if the device exists, use it, return + devNode = "/dev/" + device.Name() + stat, err = os.Stat(devNode) + if err == nil { + if stat.Mode().Type() == fs.ModeDevice { + break + } + } + // set to an empty string, so we can do validation of the last + // device too + devNode = "" + } + + // if no devices are found, or none exists in /dev, skip this part + if devNode == "" { + t.Skip("Could not find a block device under /dev") + } + + // when we get here, devNode points to an existing block device + metrics := NewMetricsBlock(devNode) + actual, err := metrics.GetMetrics() + if err != nil { + t.Errorf("Unexpected error when calling GetMetrics: %v", err) + } + + if a := actual.Capacity.Value(); a <= 0 { + t.Errorf("Expected Capacity %d to be greater than 0.", a) + } +} diff --git a/pkg/volume/metrics_errors.go b/pkg/volume/metrics_errors.go index a6cbdbf7203..0f7987e0936 100644 --- a/pkg/volume/metrics_errors.go +++ b/pkg/volume/metrics_errors.go @@ -35,6 +35,14 @@ func NewNotSupportedError() *MetricsError { } } +// NewNotImplementedError creates a new MetricsError with code NotSupported. +func NewNotImplementedError(reason string) *MetricsError { + return &MetricsError{ + Code: ErrCodeNotSupported, + Msg: fmt.Sprintf("metrics support is not implemented: %s", reason), + } +} + // NewNotSupportedErrorWithDriverName creates a new MetricsError with code NotSupported. // driver name is added to the error message. func NewNotSupportedErrorWithDriverName(name string) *MetricsError {