From 03106a152246d3346c5c00dafd137595dfa0283d Mon Sep 17 00:00:00 2001 From: Xiang Li Date: Mon, 12 Apr 2021 14:37:22 -0700 Subject: [PATCH] move filesystem resize code to kubernetes/mount-utils and add need resize function --- .../src/k8s.io/mount-utils/resizefs_linux.go | 116 +++++++ .../k8s.io/mount-utils/resizefs_linux_test.go | 301 ++++++++++++++++++ 2 files changed, 417 insertions(+) create mode 100644 staging/src/k8s.io/mount-utils/resizefs_linux_test.go diff --git a/staging/src/k8s.io/mount-utils/resizefs_linux.go b/staging/src/k8s.io/mount-utils/resizefs_linux.go index 3b2ffa31366..54a529d7c69 100644 --- a/staging/src/k8s.io/mount-utils/resizefs_linux.go +++ b/staging/src/k8s.io/mount-utils/resizefs_linux.go @@ -20,6 +20,8 @@ package mount import ( "fmt" + "strconv" + "strings" "k8s.io/klog/v2" utilexec "k8s.io/utils/exec" @@ -99,3 +101,117 @@ func (resizefs *ResizeFs) btrfsResize(deviceMountPath string) (bool, error) { resizeError := fmt.Errorf("resize of device %s failed: %v. btrfs output: %s", deviceMountPath, err, string(output)) return false, resizeError } + +func (resizefs *ResizeFs) NeedResize(devicePath string, deviceMountPath string) (bool, error) { + deviceSize, err := resizefs.getDeviceSize(devicePath) + if err != nil { + return false, err + } + var fsSize, blockSize uint64 + format, err := getDiskFormat(resizefs.exec, devicePath) + if err != nil { + formatErr := fmt.Errorf("ResizeFS.Resize - error checking format for device %s: %v", devicePath, err) + return false, formatErr + } + + // If disk has no format, there is no need to resize the disk because mkfs.* + // by default will use whole disk anyways. + if format == "" { + return false, nil + } + + klog.V(3).Infof("ResizeFs.needResize - checking mounted volume %s", devicePath) + switch format { + case "ext3", "ext4": + blockSize, fsSize, err = resizefs.getExtSize(devicePath) + klog.V(5).Infof("Ext size: filesystem size=%d, block size=%d", fsSize, blockSize) + case "xfs": + blockSize, fsSize, err = resizefs.getXFSSize(deviceMountPath) + klog.V(5).Infof("Xfs size: filesystem size=%d, block size=%d, err=%v", fsSize, blockSize, err) + default: + klog.Errorf("Not able to parse given filesystem info. fsType: %s, will not resize", format) + return false, fmt.Errorf("Could not parse fs info on given filesystem format: %s. Supported fs types are: xfs, ext3, ext4", format) + } + if err != nil { + return false, err + } + // Tolerate one block difference, just in case of rounding errors somewhere. + klog.V(5).Infof("Volume %s: device size=%d, filesystem size=%d, block size=%d", devicePath, deviceSize, fsSize, blockSize) + if deviceSize <= fsSize+blockSize { + return false, nil + } + return true, nil +} +func (resizefs *ResizeFs) getDeviceSize(devicePath string) (uint64, error) { + output, err := resizefs.exec.Command("blockdev", "--getsize64", devicePath).CombinedOutput() + outStr := strings.TrimSpace(string(output)) + if err != nil { + return 0, fmt.Errorf("failed to read size of device %s: %s: %s", devicePath, err, outStr) + } + size, err := strconv.ParseUint(outStr, 10, 64) + if err != nil { + return 0, fmt.Errorf("failed to parse size of device %s %s: %s", devicePath, outStr, err) + } + return size, nil +} + +func (resizefs *ResizeFs) getExtSize(devicePath string) (uint64, uint64, error) { + output, err := resizefs.exec.Command("dumpe2fs", "-h", devicePath).CombinedOutput() + if err != nil { + return 0, 0, fmt.Errorf("failed to read size of filesystem on %s: %s: %s", devicePath, err, string(output)) + } + + blockSize, blockCount, _ := resizefs.parseFsInfoOutput(string(output), ":", "block size", "block count") + + if blockSize == 0 { + return 0, 0, fmt.Errorf("could not find block size of device %s", devicePath) + } + if blockCount == 0 { + return 0, 0, fmt.Errorf("could not find block count of device %s", devicePath) + } + return blockSize, blockSize * blockCount, nil +} + +func (resizefs *ResizeFs) getXFSSize(devicePath string) (uint64, uint64, error) { + output, err := resizefs.exec.Command("xfs_io", "-c", "statfs", devicePath).CombinedOutput() + if err != nil { + return 0, 0, fmt.Errorf("failed to read size of filesystem on %s: %s: %s", devicePath, err, string(output)) + } + + blockSize, blockCount, _ := resizefs.parseFsInfoOutput(string(output), "=", "geom.bsize", "geom.datablocks") + + if blockSize == 0 { + return 0, 0, fmt.Errorf("could not find block size of device %s", devicePath) + } + if blockCount == 0 { + return 0, 0, fmt.Errorf("could not find block count of device %s", devicePath) + } + return blockSize, blockSize * blockCount, nil +} + +func (resizefs *ResizeFs) parseFsInfoOutput(cmdOutput string, spliter string, blockSizeKey string, blockCountKey string) (uint64, uint64, error) { + lines := strings.Split(cmdOutput, "\n") + var blockSize, blockCount uint64 + var err error + + for _, line := range lines { + tokens := strings.Split(line, spliter) + if len(tokens) != 2 { + continue + } + key, value := strings.ToLower(strings.TrimSpace(tokens[0])), strings.ToLower(strings.TrimSpace(tokens[1])) + if key == blockSizeKey { + blockSize, err = strconv.ParseUint(value, 10, 64) + if err != nil { + return 0, 0, fmt.Errorf("failed to parse block size %s: %s", value, err) + } + } + if key == blockCountKey { + blockCount, err = strconv.ParseUint(value, 10, 64) + if err != nil { + return 0, 0, fmt.Errorf("failed to parse block count %s: %s", value, err) + } + } + } + return blockSize, blockCount, err +} diff --git a/staging/src/k8s.io/mount-utils/resizefs_linux_test.go b/staging/src/k8s.io/mount-utils/resizefs_linux_test.go new file mode 100644 index 00000000000..899b72cf2c1 --- /dev/null +++ b/staging/src/k8s.io/mount-utils/resizefs_linux_test.go @@ -0,0 +1,301 @@ +// +build linux + +/* +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 mount + +import ( + "k8s.io/utils/exec" + fakeexec "k8s.io/utils/exec/testing" + "testing" +) + +func TestGetFileSystemSize(t *testing.T) { + cmdOutputSuccessXfs := + ` + statfs.f_bsize = 4096 + statfs.f_blocks = 1832448 + statfs.f_bavail = 1822366 + statfs.f_files = 3670016 + statfs.f_ffree = 3670012 + statfs.f_flags = 0x1020 + geom.bsize = 4096 + geom.agcount = 4 + geom.agblocks = 458752 + geom.datablocks = 1835008 + geom.rtblocks = 0 + geom.rtextents = 0 + geom.rtextsize = 1 + geom.sunit = 0 + geom.swidth = 0 + counts.freedata = 1822372 + counts.freertx = 0 + counts.freeino = 61 + counts.allocino = 64 +` + cmdOutputNoDataXfs := + ` + statfs.f_bsize = 4096 + statfs.f_blocks = 1832448 + statfs.f_bavail = 1822366 + statfs.f_files = 3670016 + statfs.f_ffree = 3670012 + statfs.f_flags = 0x1020 + geom.agcount = 4 + geom.agblocks = 458752 + geom.rtblocks = 0 + geom.rtextents = 0 + geom.rtextsize = 1 + geom.sunit = 0 + geom.swidth = 0 + counts.freedata = 1822372 + counts.freertx = 0 + counts.freeino = 61 + counts.allocino = 64 +` + cmdOutputSuccessExt4 := + ` +Filesystem volume name: cloudimg-rootfs +Last mounted on: / +Filesystem UUID: testUUID +Filesystem magic number: 0xEF53 +Filesystem revision #: 1 (dynamic) +Filesystem features: has_journal ext_attr resize_inode dir_index filetype needs_recovery extent 64bit +Default mount options: user_xattr acl +Filesystem state: clean +Errors behavior: Continue +Filesystem OS type: Linux +Inode count: 3840000 +Block count: 5242880 +Reserved block count: 0 +Free blocks: 5514413 +Free inodes: 3677492 +First block: 0 +Block size: 4096 +Fragment size: 4096 +Group descriptor size: 64 +Reserved GDT blocks: 252 +Blocks per group: 32768 +Fragments per group: 32768 +Inodes per group: 16000 +Inode blocks per group: 1000 +Flex block group size: 16 +Mount count: 2 +Maximum mount count: -1 +Check interval: 0 () +Lifetime writes: 180 GB +Reserved blocks uid: 0 (user root) +Reserved blocks gid: 0 (group root) +First inode: 11 +Inode size: 256 +Required extra isize: 32 +Desired extra isize: 32 +Journal inode: 8 +Default directory hash: half_md4 +Directory Hash Seed: Test Hashing +Journal backup: inode blocks +Checksum type: crc32c +Checksum: 0x57705f62 +Journal features: journal_incompat_revoke journal_64bit journal_checksum_v3 +Journal size: 64M +Journal length: 16384 +Journal sequence: 0x00037109 +Journal start: 1 +Journal checksum type: crc32c +Journal checksum: 0xb7df3c6e +` + cmdOutputNoDataExt4 := + `Filesystem volume name: cloudimg-rootfs +Last mounted on: / +Filesystem UUID: testUUID +Filesystem magic number: 0xEF53 +Filesystem revision #: 1 (dynamic) +Filesystem features: has_journal ext_attr resize_inode dir_index filetype needs_recovery extent 64bit +Default mount options: user_xattr acl +Filesystem state: clean +Errors behavior: Continue +Filesystem OS type: Linux +Inode count: 3840000 +Reserved block count: 0 +Free blocks: 5514413 +Free inodes: 3677492 +First block: 0 +Fragment size: 4096 +Group descriptor size: 64 +Reserved GDT blocks: 252 +Blocks per group: 32768 +Fragments per group: 32768 +Inodes per group: 16000 +Inode blocks per group: 1000 +Flex block group size: 16 +Mount count: 2 +Maximum mount count: -1 +Check interval: 0 () +Lifetime writes: 180 GB +Reserved blocks uid: 0 (user root) +Reserved blocks gid: 0 (group root) +First inode: 11 +Inode size: 256 +Required extra isize: 32 +Desired extra isize: 32 +Journal inode: 8 +Default directory hash: half_md4 +Directory Hash Seed: Test Hashing +Journal backup: inode blocks +Checksum type: crc32c +Checksum: 0x57705f62 +Journal features: journal_incompat_revoke journal_64bit journal_checksum_v3 +Journal size: 64M +Journal length: 16384 +Journal sequence: 0x00037109 +Journal start: 1 +Journal checksum type: crc32c +Journal checksum: 0xb7df3c6e +` + testcases := []struct { + name string + devicePath string + blocksize uint64 + blockCount uint64 + cmdOutput string + expectError bool + fsType string + }{ + { + name: "success parse xfs info", + devicePath: "/dev/test1", + blocksize: 4096, + blockCount: 1835008, + cmdOutput: cmdOutputSuccessXfs, + expectError: false, + fsType: "xfs", + }, + { + name: "block size not present - xfs", + devicePath: "/dev/test1", + blocksize: 0, + blockCount: 0, + cmdOutput: cmdOutputNoDataXfs, + expectError: true, + fsType: "xfs", + }, + { + name: "success parse ext info", + devicePath: "/dev/test1", + blocksize: 4096, + blockCount: 5242880, + cmdOutput: cmdOutputSuccessExt4, + expectError: false, + fsType: "ext4", + }, + { + name: "block size not present - ext4", + devicePath: "/dev/test1", + blocksize: 0, + blockCount: 0, + cmdOutput: cmdOutputNoDataExt4, + expectError: true, + fsType: "ext4", + }, + } + + for _, test := range testcases { + t.Run(test.name, func(t *testing.T) { + fcmd := fakeexec.FakeCmd{ + CombinedOutputScript: []fakeexec.FakeAction{ + func() ([]byte, []byte, error) { return []byte(test.cmdOutput), nil, nil }, + }, + } + fexec := fakeexec.FakeExec{ + CommandScript: []fakeexec.FakeCommandAction{ + func(cmd string, args ...string) exec.Cmd { + return fakeexec.InitFakeCmd(&fcmd, cmd, args...) + }, + }, + } + resizefs := ResizeFs{exec: &fexec} + + var blockSize uint64 + var fsSize uint64 + var err error + switch test.fsType { + case "xfs": + blockSize, fsSize, err = resizefs.getXFSSize(test.devicePath) + case "ext4": + blockSize, fsSize, err = resizefs.getExtSize(test.devicePath) + } + + if blockSize != test.blocksize { + t.Fatalf("Parse wrong block size value, expect %d, but got %d", test.blocksize, blockSize) + } + if fsSize != test.blocksize*test.blockCount { + t.Fatalf("Parse wrong fs size value, expect %d, but got %d", test.blocksize*test.blockCount, fsSize) + } + if !test.expectError && err != nil { + t.Fatalf("Expect no error but got %v", err) + } + }) + } +} + +func TestNeedResize(t *testing.T) { + testcases := []struct { + name string + devicePath string + deviceMountPath string + deviceSize string + cmdOutputFsType string + expectError bool + expectResult bool + }{ + { + name: "False - Unsupported fs type", + devicePath: "/dev/test1", + deviceMountPath: "/mnt/test1", + deviceSize: "2048", + cmdOutputFsType: "TYPE=ntfs", + expectError: true, + expectResult: false, + }, + } + + for _, test := range testcases { + t.Run(test.name, func(t *testing.T) { + fcmd := fakeexec.FakeCmd{ + CombinedOutputScript: []fakeexec.FakeAction{ + func() ([]byte, []byte, error) { return []byte(test.deviceSize), nil, nil }, + func() ([]byte, []byte, error) { return []byte(test.cmdOutputFsType), nil, nil }, + }, + } + fexec := fakeexec.FakeExec{ + CommandScript: []fakeexec.FakeCommandAction{ + func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) }, + func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) }, + }, + } + resizefs := ResizeFs{exec: &fexec} + + needResize, err := resizefs.NeedResize(test.devicePath, test.deviceMountPath) + if needResize != test.expectResult { + t.Fatalf("Expect result is %v but got %v", test.expectResult, needResize) + } + if !test.expectError && err != nil { + t.Fatalf("Expect no error but got %v", err) + } + }) + } +}