k8s.io/kubernetes@v1.29.3/test/e2e/storage/utils/local.go (about) 1 /* 2 Copyright 2019 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package utils 18 19 /* 20 * Various local test resource implementations. 21 */ 22 23 import ( 24 "context" 25 "fmt" 26 "path/filepath" 27 "strings" 28 29 "github.com/onsi/ginkgo/v2" 30 v1 "k8s.io/api/core/v1" 31 "k8s.io/apimachinery/pkg/util/uuid" 32 "k8s.io/kubernetes/test/e2e/framework" 33 ) 34 35 // LocalVolumeType represents type of local volume, e.g. tmpfs, directory, 36 // block, etc. 37 type LocalVolumeType string 38 39 const ( 40 // LocalVolumeDirectory reprensents a simple directory as local volume 41 LocalVolumeDirectory LocalVolumeType = "dir" 42 // LocalVolumeDirectoryLink is like LocalVolumeDirectory but it's a symbolic link to directory 43 LocalVolumeDirectoryLink LocalVolumeType = "dir-link" 44 // LocalVolumeDirectoryBindMounted is like LocalVolumeDirectory but bind mounted 45 LocalVolumeDirectoryBindMounted LocalVolumeType = "dir-bindmounted" 46 // LocalVolumeDirectoryLinkBindMounted is like LocalVolumeDirectory but it's a symbolic link to self bind mounted directory 47 // Note that bind mounting at symbolic link actually mounts at directory it 48 // links to 49 LocalVolumeDirectoryLinkBindMounted LocalVolumeType = "dir-link-bindmounted" 50 // LocalVolumeTmpfs represents a temporary filesystem to be used as local volume 51 LocalVolumeTmpfs LocalVolumeType = "tmpfs" 52 // LocalVolumeBlock represents a Block device, creates a local file, and maps it as a block device 53 LocalVolumeBlock LocalVolumeType = "block" 54 // LocalVolumeBlockFS represents a filesystem backed by a block device 55 LocalVolumeBlockFS LocalVolumeType = "blockfs" 56 // LocalVolumeGCELocalSSD represents a Filesystem backed by GCE Local SSD as local volume 57 LocalVolumeGCELocalSSD LocalVolumeType = "gce-localssd-scsi-fs" 58 ) 59 60 // LocalTestResource represents test resource of a local volume. 61 type LocalTestResource struct { 62 VolumeType LocalVolumeType 63 Node *v1.Node 64 // Volume path, path to filesystem or block device on the node 65 Path string 66 // If volume is backed by a loop device, we create loop device storage file 67 // under this directory. 68 loopDir string 69 } 70 71 // LocalTestResourceManager represents interface to create/destroy local test resources on node 72 type LocalTestResourceManager interface { 73 Create(ctx context.Context, node *v1.Node, volumeType LocalVolumeType, parameters map[string]string) *LocalTestResource 74 ExpandBlockDevice(ctx context.Context, ltr *LocalTestResource, mbToAdd int) error 75 Remove(ctx context.Context, ltr *LocalTestResource) 76 } 77 78 // ltrMgr implements LocalTestResourceManager 79 type ltrMgr struct { 80 prefix string 81 hostExec HostExec 82 // hostBase represents a writable directory on the host under which we 83 // create test directories 84 hostBase string 85 } 86 87 // NewLocalResourceManager returns a instance of LocalTestResourceManager 88 func NewLocalResourceManager(prefix string, hostExec HostExec, hostBase string) LocalTestResourceManager { 89 return <rMgr{ 90 prefix: prefix, 91 hostExec: hostExec, 92 hostBase: hostBase, 93 } 94 } 95 96 // getTestDir returns a test dir under `hostBase` directory with randome name. 97 func (l *ltrMgr) getTestDir() string { 98 testDirName := fmt.Sprintf("%s-%s", l.prefix, string(uuid.NewUUID())) 99 return filepath.Join(l.hostBase, testDirName) 100 } 101 102 func (l *ltrMgr) setupLocalVolumeTmpfs(ctx context.Context, node *v1.Node, parameters map[string]string) *LocalTestResource { 103 hostDir := l.getTestDir() 104 ginkgo.By(fmt.Sprintf("Creating tmpfs mount point on node %q at path %q", node.Name, hostDir)) 105 err := l.hostExec.IssueCommand(ctx, fmt.Sprintf("mkdir -p %q && mount -t tmpfs -o size=10m tmpfs-%q %q", hostDir, hostDir, hostDir), node) 106 framework.ExpectNoError(err) 107 return &LocalTestResource{ 108 Node: node, 109 Path: hostDir, 110 } 111 } 112 113 func (l *ltrMgr) cleanupLocalVolumeTmpfs(ctx context.Context, ltr *LocalTestResource) { 114 ginkgo.By(fmt.Sprintf("Unmount tmpfs mount point on node %q at path %q", ltr.Node.Name, ltr.Path)) 115 err := l.hostExec.IssueCommand(ctx, fmt.Sprintf("umount %q", ltr.Path), ltr.Node) 116 framework.ExpectNoError(err) 117 118 ginkgo.By("Removing the test directory") 119 err = l.hostExec.IssueCommand(ctx, fmt.Sprintf("rm -r %s", ltr.Path), ltr.Node) 120 framework.ExpectNoError(err) 121 } 122 123 // createAndSetupLoopDevice creates an empty file and associates a loop devie with it. 124 func (l *ltrMgr) createAndSetupLoopDevice(ctx context.Context, dir string, node *v1.Node, size int) { 125 ginkgo.By(fmt.Sprintf("Creating block device on node %q using path %q", node.Name, dir)) 126 mkdirCmd := fmt.Sprintf("mkdir -p %s", dir) 127 count := size / 4096 128 // xfs requires at least 4096 blocks 129 if count < 4096 { 130 count = 4096 131 } 132 ddCmd := fmt.Sprintf("dd if=/dev/zero of=%s/file bs=4096 count=%d", dir, count) 133 losetupCmd := fmt.Sprintf("losetup -f %s/file", dir) 134 err := l.hostExec.IssueCommand(ctx, fmt.Sprintf("%s && %s && %s", mkdirCmd, ddCmd, losetupCmd), node) 135 framework.ExpectNoError(err) 136 } 137 138 // findLoopDevice finds loop device path by its associated storage directory. 139 func (l *ltrMgr) findLoopDevice(ctx context.Context, dir string, node *v1.Node) string { 140 cmd := fmt.Sprintf("E2E_LOOP_DEV=$(losetup | grep %s/file | awk '{ print $1 }') 2>&1 > /dev/null && echo ${E2E_LOOP_DEV}", dir) 141 loopDevResult, err := l.hostExec.IssueCommandWithResult(ctx, cmd, node) 142 framework.ExpectNoError(err) 143 return strings.TrimSpace(loopDevResult) 144 } 145 146 func (l *ltrMgr) setupLocalVolumeBlock(ctx context.Context, node *v1.Node, parameters map[string]string) *LocalTestResource { 147 loopDir := l.getTestDir() 148 l.createAndSetupLoopDevice(ctx, loopDir, node, 20*1024*1024) 149 loopDev := l.findLoopDevice(ctx, loopDir, node) 150 return &LocalTestResource{ 151 Node: node, 152 Path: loopDev, 153 loopDir: loopDir, 154 } 155 } 156 157 // teardownLoopDevice tears down loop device by its associated storage directory. 158 func (l *ltrMgr) teardownLoopDevice(ctx context.Context, dir string, node *v1.Node) { 159 loopDev := l.findLoopDevice(ctx, dir, node) 160 ginkgo.By(fmt.Sprintf("Tear down block device %q on node %q at path %s/file", loopDev, node.Name, dir)) 161 losetupDeleteCmd := fmt.Sprintf("losetup -d %s", loopDev) 162 err := l.hostExec.IssueCommand(ctx, losetupDeleteCmd, node) 163 framework.ExpectNoError(err) 164 return 165 } 166 167 func (l *ltrMgr) cleanupLocalVolumeBlock(ctx context.Context, ltr *LocalTestResource) { 168 l.teardownLoopDevice(ctx, ltr.loopDir, ltr.Node) 169 ginkgo.By(fmt.Sprintf("Removing the test directory %s", ltr.loopDir)) 170 removeCmd := fmt.Sprintf("rm -r %s", ltr.loopDir) 171 err := l.hostExec.IssueCommand(ctx, removeCmd, ltr.Node) 172 framework.ExpectNoError(err) 173 } 174 175 func (l *ltrMgr) setupLocalVolumeBlockFS(ctx context.Context, node *v1.Node, parameters map[string]string) *LocalTestResource { 176 ltr := l.setupLocalVolumeBlock(ctx, node, parameters) 177 loopDev := ltr.Path 178 loopDir := ltr.loopDir 179 // Format and mount at loopDir and give others rwx for read/write testing 180 cmd := fmt.Sprintf("mkfs -t ext4 %s && mount -t ext4 %s %s && chmod o+rwx %s", loopDev, loopDev, loopDir, loopDir) 181 err := l.hostExec.IssueCommand(ctx, cmd, node) 182 framework.ExpectNoError(err) 183 return &LocalTestResource{ 184 Node: node, 185 Path: loopDir, 186 loopDir: loopDir, 187 } 188 } 189 190 func (l *ltrMgr) cleanupLocalVolumeBlockFS(ctx context.Context, ltr *LocalTestResource) { 191 umountCmd := fmt.Sprintf("umount %s", ltr.Path) 192 err := l.hostExec.IssueCommand(ctx, umountCmd, ltr.Node) 193 framework.ExpectNoError(err) 194 l.cleanupLocalVolumeBlock(ctx, ltr) 195 } 196 197 func (l *ltrMgr) setupLocalVolumeDirectory(ctx context.Context, node *v1.Node, parameters map[string]string) *LocalTestResource { 198 hostDir := l.getTestDir() 199 mkdirCmd := fmt.Sprintf("mkdir -p %s", hostDir) 200 err := l.hostExec.IssueCommand(ctx, mkdirCmd, node) 201 framework.ExpectNoError(err) 202 return &LocalTestResource{ 203 Node: node, 204 Path: hostDir, 205 } 206 } 207 208 func (l *ltrMgr) cleanupLocalVolumeDirectory(ctx context.Context, ltr *LocalTestResource) { 209 ginkgo.By("Removing the test directory") 210 removeCmd := fmt.Sprintf("rm -r %s", ltr.Path) 211 err := l.hostExec.IssueCommand(ctx, removeCmd, ltr.Node) 212 framework.ExpectNoError(err) 213 } 214 215 func (l *ltrMgr) setupLocalVolumeDirectoryLink(ctx context.Context, node *v1.Node, parameters map[string]string) *LocalTestResource { 216 hostDir := l.getTestDir() 217 hostDirBackend := hostDir + "-backend" 218 cmd := fmt.Sprintf("mkdir %s && ln -s %s %s", hostDirBackend, hostDirBackend, hostDir) 219 err := l.hostExec.IssueCommand(ctx, cmd, node) 220 framework.ExpectNoError(err) 221 return &LocalTestResource{ 222 Node: node, 223 Path: hostDir, 224 } 225 } 226 227 func (l *ltrMgr) cleanupLocalVolumeDirectoryLink(ctx context.Context, ltr *LocalTestResource) { 228 ginkgo.By("Removing the test directory") 229 hostDir := ltr.Path 230 hostDirBackend := hostDir + "-backend" 231 removeCmd := fmt.Sprintf("rm -r %s && rm -r %s", hostDir, hostDirBackend) 232 err := l.hostExec.IssueCommand(ctx, removeCmd, ltr.Node) 233 framework.ExpectNoError(err) 234 } 235 236 func (l *ltrMgr) setupLocalVolumeDirectoryBindMounted(ctx context.Context, node *v1.Node, parameters map[string]string) *LocalTestResource { 237 hostDir := l.getTestDir() 238 cmd := fmt.Sprintf("mkdir %s && mount --bind %s %s", hostDir, hostDir, hostDir) 239 err := l.hostExec.IssueCommand(ctx, cmd, node) 240 framework.ExpectNoError(err) 241 return &LocalTestResource{ 242 Node: node, 243 Path: hostDir, 244 } 245 } 246 247 func (l *ltrMgr) cleanupLocalVolumeDirectoryBindMounted(ctx context.Context, ltr *LocalTestResource) { 248 ginkgo.By("Removing the test directory") 249 hostDir := ltr.Path 250 removeCmd := fmt.Sprintf("umount %s && rm -r %s", hostDir, hostDir) 251 err := l.hostExec.IssueCommand(ctx, removeCmd, ltr.Node) 252 framework.ExpectNoError(err) 253 } 254 255 func (l *ltrMgr) setupLocalVolumeDirectoryLinkBindMounted(ctx context.Context, node *v1.Node, parameters map[string]string) *LocalTestResource { 256 hostDir := l.getTestDir() 257 hostDirBackend := hostDir + "-backend" 258 cmd := fmt.Sprintf("mkdir %s && mount --bind %s %s && ln -s %s %s", hostDirBackend, hostDirBackend, hostDirBackend, hostDirBackend, hostDir) 259 err := l.hostExec.IssueCommand(ctx, cmd, node) 260 framework.ExpectNoError(err) 261 return &LocalTestResource{ 262 Node: node, 263 Path: hostDir, 264 } 265 } 266 267 func (l *ltrMgr) cleanupLocalVolumeDirectoryLinkBindMounted(ctx context.Context, ltr *LocalTestResource) { 268 ginkgo.By("Removing the test directory") 269 hostDir := ltr.Path 270 hostDirBackend := hostDir + "-backend" 271 removeCmd := fmt.Sprintf("rm %s && umount %s && rm -r %s", hostDir, hostDirBackend, hostDirBackend) 272 err := l.hostExec.IssueCommand(ctx, removeCmd, ltr.Node) 273 framework.ExpectNoError(err) 274 } 275 276 func (l *ltrMgr) setupLocalVolumeGCELocalSSD(ctx context.Context, node *v1.Node, parameters map[string]string) *LocalTestResource { 277 res, err := l.hostExec.IssueCommandWithResult(ctx, "ls /mnt/disks/by-uuid/google-local-ssds-scsi-fs/", node) 278 framework.ExpectNoError(err) 279 dirName := strings.Fields(res)[0] 280 hostDir := "/mnt/disks/by-uuid/google-local-ssds-scsi-fs/" + dirName 281 return &LocalTestResource{ 282 Node: node, 283 Path: hostDir, 284 } 285 } 286 287 func (l *ltrMgr) cleanupLocalVolumeGCELocalSSD(ctx context.Context, ltr *LocalTestResource) { 288 // This filesystem is attached in cluster initialization, we clean all files to make it reusable. 289 removeCmd := fmt.Sprintf("find '%s' -mindepth 1 -maxdepth 1 -print0 | xargs -r -0 rm -rf", ltr.Path) 290 err := l.hostExec.IssueCommand(ctx, removeCmd, ltr.Node) 291 framework.ExpectNoError(err) 292 } 293 294 func (l *ltrMgr) expandLocalVolumeBlockFS(ctx context.Context, ltr *LocalTestResource, mbToAdd int) error { 295 ddCmd := fmt.Sprintf("dd if=/dev/zero of=%s/file conv=notrunc oflag=append bs=1M count=%d", ltr.loopDir, mbToAdd) 296 loopDev := l.findLoopDevice(ctx, ltr.loopDir, ltr.Node) 297 losetupCmd := fmt.Sprintf("losetup -c %s", loopDev) 298 return l.hostExec.IssueCommand(ctx, fmt.Sprintf("%s && %s", ddCmd, losetupCmd), ltr.Node) 299 } 300 301 func (l *ltrMgr) ExpandBlockDevice(ctx context.Context, ltr *LocalTestResource, mbtoAdd int) error { 302 switch ltr.VolumeType { 303 case LocalVolumeBlockFS: 304 return l.expandLocalVolumeBlockFS(ctx, ltr, mbtoAdd) 305 } 306 return fmt.Errorf("Failed to expand local test resource, unsupported volume type: %s", ltr.VolumeType) 307 } 308 309 func (l *ltrMgr) Create(ctx context.Context, node *v1.Node, volumeType LocalVolumeType, parameters map[string]string) *LocalTestResource { 310 var ltr *LocalTestResource 311 switch volumeType { 312 case LocalVolumeDirectory: 313 ltr = l.setupLocalVolumeDirectory(ctx, node, parameters) 314 case LocalVolumeDirectoryLink: 315 ltr = l.setupLocalVolumeDirectoryLink(ctx, node, parameters) 316 case LocalVolumeDirectoryBindMounted: 317 ltr = l.setupLocalVolumeDirectoryBindMounted(ctx, node, parameters) 318 case LocalVolumeDirectoryLinkBindMounted: 319 ltr = l.setupLocalVolumeDirectoryLinkBindMounted(ctx, node, parameters) 320 case LocalVolumeTmpfs: 321 ltr = l.setupLocalVolumeTmpfs(ctx, node, parameters) 322 case LocalVolumeBlock: 323 ltr = l.setupLocalVolumeBlock(ctx, node, parameters) 324 case LocalVolumeBlockFS: 325 ltr = l.setupLocalVolumeBlockFS(ctx, node, parameters) 326 case LocalVolumeGCELocalSSD: 327 ltr = l.setupLocalVolumeGCELocalSSD(ctx, node, parameters) 328 default: 329 framework.Failf("Failed to create local test resource on node %q, unsupported volume type: %v is specified", node.Name, volumeType) 330 return nil 331 } 332 if ltr == nil { 333 framework.Failf("Failed to create local test resource on node %q, volume type: %v, parameters: %v", node.Name, volumeType, parameters) 334 } 335 ltr.VolumeType = volumeType 336 return ltr 337 } 338 339 func (l *ltrMgr) Remove(ctx context.Context, ltr *LocalTestResource) { 340 switch ltr.VolumeType { 341 case LocalVolumeDirectory: 342 l.cleanupLocalVolumeDirectory(ctx, ltr) 343 case LocalVolumeDirectoryLink: 344 l.cleanupLocalVolumeDirectoryLink(ctx, ltr) 345 case LocalVolumeDirectoryBindMounted: 346 l.cleanupLocalVolumeDirectoryBindMounted(ctx, ltr) 347 case LocalVolumeDirectoryLinkBindMounted: 348 l.cleanupLocalVolumeDirectoryLinkBindMounted(ctx, ltr) 349 case LocalVolumeTmpfs: 350 l.cleanupLocalVolumeTmpfs(ctx, ltr) 351 case LocalVolumeBlock: 352 l.cleanupLocalVolumeBlock(ctx, ltr) 353 case LocalVolumeBlockFS: 354 l.cleanupLocalVolumeBlockFS(ctx, ltr) 355 case LocalVolumeGCELocalSSD: 356 l.cleanupLocalVolumeGCELocalSSD(ctx, ltr) 357 default: 358 framework.Failf("Failed to remove local test resource, unsupported volume type: %v is specified", ltr.VolumeType) 359 } 360 return 361 }