github.com/canonical/ubuntu-image@v0.0.0-20240430122802-2202fe98b290/internal/statemachine/common_test.go (about) 1 // This file contains unit tests for all of the common state functions 2 package statemachine 3 4 import ( 5 "bytes" 6 "crypto/rand" 7 "os" 8 "os/exec" 9 "path/filepath" 10 "strconv" 11 "strings" 12 "testing" 13 14 diskfs "github.com/diskfs/go-diskfs" 15 "github.com/google/uuid" 16 "github.com/snapcore/snapd/gadget" 17 "github.com/snapcore/snapd/gadget/quantity" 18 "github.com/snapcore/snapd/osutil" 19 20 "github.com/canonical/ubuntu-image/internal/helper" 21 ) 22 23 // TestLoadGadgetYaml tests a successful load of gadget.yaml. It also tests that the unpack 24 // directory is preserved if the relevant environment variable is set 25 func TestLoadGadgetYaml(t *testing.T) { 26 asserter := helper.Asserter{T: t} 27 var stateMachine StateMachine 28 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 29 stateMachine.YamlFilePath = filepath.Join("testdata", "gadget_tree", "meta", "gadget.yaml") 30 31 err := stateMachine.makeTemporaryDirectories() 32 asserter.AssertErrNil(err, true) 33 34 preserveDir := filepath.Join("/tmp", "ubuntu-image-"+uuid.NewString()) 35 os.Setenv("UBUNTU_IMAGE_PRESERVE_UNPACK", preserveDir) 36 defer func() { 37 os.Unsetenv("UBUNTU_IMAGE_PRESERVE_UNPACK") 38 }() 39 // ensure unpack exists 40 err = os.MkdirAll(stateMachine.tempDirs.unpack, 0755) 41 asserter.AssertErrNil(err, true) 42 t.Cleanup(func() { os.RemoveAll(preserveDir) }) 43 err = stateMachine.loadGadgetYaml() 44 asserter.AssertErrNil(err, true) 45 46 // check that unpack was preserved 47 preserveUnpack := filepath.Join(preserveDir, "unpack") 48 if _, err := os.Stat(preserveUnpack); err != nil { 49 t.Errorf("Preserve unpack directory %s does not exist", preserveUnpack) 50 } 51 os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) 52 } 53 54 // TestFailedLoadGadgetYaml tests failures in the loadGadgetYaml state 55 // This is achieved by providing an invalid gadget.yaml and mocking 56 // os.MkdirAll, iotuil.ReadFile, osutil.CopyFile, and osutil.CopySpecialFile 57 func TestFailedLoadGadgetYaml(t *testing.T) { 58 asserter := helper.Asserter{T: t} 59 var stateMachine StateMachine 60 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 61 62 err := stateMachine.makeTemporaryDirectories() 63 asserter.AssertErrNil(err, true) 64 65 stateMachine.YamlFilePath = filepath.Join("testdata", 66 "gadget_tree", "meta", "gadget.yaml") 67 // mock osutil.CopySpecialFile 68 osutilCopyFile = mockCopyFile 69 defer func() { 70 osutilCopyFile = osutil.CopyFile 71 }() 72 err = stateMachine.loadGadgetYaml() 73 asserter.AssertErrContains(err, "Error copying gadget.yaml") 74 asserter.AssertErrContains(err, "\nThe gadget.yaml file is expected to be located in a \"meta\" subdirectory of the provided built gadget directory.\n") 75 osutilCopyFile = osutil.CopyFile 76 77 // mock osReadFile 78 osReadFile = mockReadFile 79 defer func() { 80 osReadFile = os.ReadFile 81 }() 82 err = stateMachine.loadGadgetYaml() 83 asserter.AssertErrContains(err, "Error reading gadget.yaml bytes") 84 osReadFile = os.ReadFile 85 86 // now test with the invalid yaml file 87 stateMachine.YamlFilePath = filepath.Join("testdata", 88 "gadget_tree_invalid", "meta", "gadget.yaml") 89 err = stateMachine.loadGadgetYaml() 90 asserter.AssertErrContains(err, "Error running InfoFromGadgetYaml") 91 92 // set a valid yaml file and preserveDir 93 stateMachine.YamlFilePath = filepath.Join("testdata", 94 "gadget_tree", "meta", "gadget.yaml") 95 96 // mock os.MkdirAll 97 osMkdirAll = mockMkdirAll 98 defer func() { 99 osMkdirAll = os.MkdirAll 100 }() 101 // run with and without the environment variable set 102 err = stateMachine.loadGadgetYaml() 103 asserter.AssertErrContains(err, "Error creating volume dir") 104 105 preserveDir := filepath.Join("/tmp", "ubuntu-image-"+uuid.NewString()) 106 os.Setenv("UBUNTU_IMAGE_PRESERVE_UNPACK", preserveDir) 107 defer func() { 108 os.Unsetenv("UBUNTU_IMAGE_PRESERVE_UNPACK") 109 }() 110 t.Cleanup(func() { os.RemoveAll(preserveDir) }) 111 err = stateMachine.loadGadgetYaml() 112 asserter.AssertErrContains(err, "Error creating preserve unpack directory") 113 osMkdirAll = os.MkdirAll 114 115 // mock osutil.CopySpecialFile 116 osutilCopySpecialFile = mockCopySpecialFile 117 defer func() { 118 osutilCopySpecialFile = osutil.CopySpecialFile 119 }() 120 err = stateMachine.loadGadgetYaml() 121 asserter.AssertErrContains(err, "Error preserving unpack dir") 122 osutilCopySpecialFile = osutil.CopySpecialFile 123 os.Unsetenv("UBUNTU_IMAGE_PRESERVE_UNPACK") 124 125 // set an invalid --image-size argument to cause a failure 126 stateMachine.commonFlags.Size = "test" 127 err = stateMachine.loadGadgetYaml() 128 asserter.AssertErrContains(err, "Failed to parse argument to --image-size") 129 130 os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) 131 } 132 133 // TestGenerateDiskInfo tests that diskInfo can be generated 134 func TestGenerateDiskInfo(t *testing.T) { 135 asserter := helper.Asserter{T: t} 136 var stateMachine StateMachine 137 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 138 stateMachine.commonFlags.DiskInfo = filepath.Join("testdata", "disk_info") 139 140 err := stateMachine.makeTemporaryDirectories() 141 asserter.AssertErrNil(err, true) 142 143 t.Cleanup(func() { os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) }) 144 145 err = stateMachine.generateDiskInfo() 146 asserter.AssertErrNil(err, true) 147 148 // make sure rootfs/.disk/info exists 149 _, err = os.Stat(filepath.Join(stateMachine.tempDirs.rootfs, ".disk", "info")) 150 if err != nil { 151 if os.IsNotExist(err) { 152 t.Errorf("Disk Info file should exist, but does not") 153 } 154 } 155 156 os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) 157 } 158 159 // TestFailedGenerateDiskInfo tests failure scenarios in the generate_disk_info state 160 func TestFailedGenerateDiskInfo(t *testing.T) { 161 asserter := helper.Asserter{T: t} 162 var stateMachine StateMachine 163 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 164 stateMachine.commonFlags.DiskInfo = filepath.Join("testdata", "fake_disk_info") 165 166 err := stateMachine.makeTemporaryDirectories() 167 asserter.AssertErrNil(err, true) 168 169 // mock os.Mkdir 170 osMkdir = mockMkdir 171 defer func() { 172 osMkdir = os.Mkdir 173 }() 174 err = stateMachine.generateDiskInfo() 175 asserter.AssertErrContains(err, "Failed to create disk info directory") 176 osMkdir = os.Mkdir 177 178 // mock osutil.CopyFile 179 osutilCopyFile = mockCopyFile 180 defer func() { 181 osutilCopyFile = osutil.CopyFile 182 }() 183 err = stateMachine.generateDiskInfo() 184 asserter.AssertErrContains(err, "Failed to copy Disk Info file") 185 osutilCopyFile = osutil.CopyFile 186 187 os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) 188 } 189 190 // TestCalculateRootfsSizeNoImageSize tests that the rootfs size can be 191 // calculated by using du commands when the image size is not specified 192 // this is accomplished by setting the test gadget tree as rootfs and 193 // verifying that the size is calculated correctly 194 func TestCalculateRootfsSizeNoImageSize(t *testing.T) { 195 asserter := helper.Asserter{T: t} 196 var stateMachine StateMachine 197 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 198 stateMachine.tempDirs.rootfs = filepath.Join("testdata", "gadget_tree") 199 200 err := stateMachine.makeTemporaryDirectories() 201 asserter.AssertErrNil(err, true) 202 203 // set a valid yaml file and load it in 204 stateMachine.YamlFilePath = filepath.Join("testdata", 205 "gadget_tree", "meta", "gadget.yaml") 206 // ensure unpack exists 207 err = os.MkdirAll(filepath.Join(stateMachine.tempDirs.unpack, "gadget"), 0755) 208 asserter.AssertErrNil(err, true) 209 err = stateMachine.loadGadgetYaml() 210 asserter.AssertErrNil(err, true) 211 212 err = stateMachine.calculateRootfsSize() 213 asserter.AssertErrNil(err, true) 214 215 // rootfs size will be slightly different in different environments 216 correctSizeLower, err := quantity.ParseSize("8M") 217 asserter.AssertErrNil(err, true) 218 correctSizeUpper := correctSizeLower + 100000 // 0.1 MB range 219 if stateMachine.RootfsSize > correctSizeUpper || 220 stateMachine.RootfsSize < correctSizeLower { 221 t.Errorf("expected rootfs size between %s and %s, got %s", 222 correctSizeLower.IECString(), 223 correctSizeUpper.IECString(), 224 stateMachine.RootfsSize.IECString()) 225 } 226 227 os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) 228 } 229 230 // TestCalculateRootfsSizeImageSize tests that the rootfs size can be 231 // accurately calculated when the image size is specified 232 func TestCalculateRootfsSizeImageSize(t *testing.T) { 233 testCases := []struct { 234 name string 235 sizeArg string 236 expectedSize quantity.Size 237 }{ 238 {"one_image_size", "4G", 4183818240}, 239 {"image_size_per_volume", "pc:4G", 4183818240}, 240 } 241 for _, tc := range testCases { 242 t.Run("test_calculate_rootfs_size_image_size", func(t *testing.T) { 243 asserter := helper.Asserter{T: t} 244 var stateMachine StateMachine 245 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 246 stateMachine.tempDirs.rootfs = filepath.Join("testdata", "gadget_tree") 247 stateMachine.commonFlags.Size = tc.sizeArg 248 249 err := stateMachine.makeTemporaryDirectories() 250 asserter.AssertErrNil(err, true) 251 252 // set a valid yaml file and load it in 253 stateMachine.YamlFilePath = filepath.Join("testdata", 254 "gadget_tree", "meta", "gadget.yaml") 255 // ensure unpack exists 256 err = os.MkdirAll(filepath.Join(stateMachine.tempDirs.unpack, "gadget"), 0755) 257 asserter.AssertErrNil(err, true) 258 err = stateMachine.loadGadgetYaml() 259 asserter.AssertErrNil(err, true) 260 261 err = stateMachine.calculateRootfsSize() 262 asserter.AssertErrNil(err, true) 263 264 if stateMachine.RootfsSize != tc.expectedSize { 265 t.Errorf("Expected rootfs size %d, but got %d", 266 tc.expectedSize, stateMachine.RootfsSize) 267 } 268 269 os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) 270 }) 271 } 272 } 273 274 // TestFailedCalculateRootfsSize tests a failure when calculating the rootfs size 275 // this is accomplished by setting rootfs to a directory that does not exist 276 func TestFailedCalculateRootfsSize(t *testing.T) { 277 asserter := helper.Asserter{T: t} 278 var stateMachine StateMachine 279 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 280 stateMachine.tempDirs.rootfs = filepath.Join("testdata", uuid.NewString()) 281 282 err := stateMachine.calculateRootfsSize() 283 asserter.AssertErrContains(err, "Error getting rootfs size") 284 285 // now set a value of --image-size that is too small to hold the rootfs 286 stateMachine.commonFlags.Size = "1M" 287 288 err = stateMachine.makeTemporaryDirectories() 289 asserter.AssertErrNil(err, true) 290 291 // set a valid yaml file and load it in 292 stateMachine.YamlFilePath = filepath.Join("testdata", 293 "gadget_tree", "meta", "gadget.yaml") 294 // ensure unpack exists 295 err = os.MkdirAll(filepath.Join(stateMachine.tempDirs.unpack, "gadget"), 0755) 296 asserter.AssertErrNil(err, true) 297 err = stateMachine.loadGadgetYaml() 298 asserter.AssertErrNil(err, true) 299 300 err = stateMachine.calculateRootfsSize() 301 asserter.AssertErrContains(err, "smaller than actual rootfs contents") 302 } 303 304 // TestPopulateBootfsContents tests a successful run of the populateBootfsContents state 305 // and ensures that the appropriate files are placed in the bootfs 306 func TestPopulateBootfsContents(t *testing.T) { 307 asserter := helper.Asserter{T: t} 308 var stateMachine StateMachine 309 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 310 311 err := stateMachine.makeTemporaryDirectories() 312 asserter.AssertErrNil(err, true) 313 t.Cleanup(func() { os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) }) 314 315 // set a valid yaml file and load it in 316 stateMachine.YamlFilePath = filepath.Join("testdata", 317 "gadget_tree", "meta", "gadget.yaml") 318 // ensure unpack exists 319 err = os.MkdirAll(filepath.Join(stateMachine.tempDirs.unpack, "gadget"), 0755) 320 asserter.AssertErrNil(err, true) 321 err = stateMachine.loadGadgetYaml() 322 asserter.AssertErrNil(err, true) 323 324 // populate unpack 325 files, err := os.ReadDir(filepath.Join("testdata", "gadget_tree")) 326 asserter.AssertErrNil(err, true) 327 for _, srcFile := range files { 328 srcFile := filepath.Join("testdata", "gadget_tree", srcFile.Name()) 329 err = osutilCopySpecialFile(srcFile, filepath.Join(stateMachine.tempDirs.unpack, "gadget")) 330 asserter.AssertErrNil(err, true) 331 } 332 333 // ensure volumes exists 334 err = os.MkdirAll(stateMachine.tempDirs.volumes, 0755) 335 asserter.AssertErrNil(err, true) 336 err = stateMachine.populateBootfsContents() 337 asserter.AssertErrNil(err, true) 338 339 // check that bootfs contents were actually populated 340 bootFiles := []string{"boot", "ubuntu"} 341 for _, file := range bootFiles { 342 fullPath := filepath.Join(stateMachine.tempDirs.volumes, 343 "pc", "part2", "EFI", file) 344 if _, err := os.Stat(fullPath); err != nil { 345 t.Errorf("Expected %s to exist, but it does not", fullPath) 346 } 347 } 348 } 349 350 // TestPopulateBootfsContentsPiboot tests a successful run of the 351 // populateBootfsContents state and ensures that the appropriate files are 352 // placed in the bootfs, for the piboot bootloader. 353 func TestPopulateBootfsContentsPiboot(t *testing.T) { 354 asserter := helper.Asserter{T: t} 355 var stateMachine StateMachine 356 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 357 358 err := stateMachine.makeTemporaryDirectories() 359 asserter.AssertErrNil(err, true) 360 t.Cleanup(func() { os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) }) 361 362 // set a valid yaml file and load it in 363 stateMachine.YamlFilePath = filepath.Join("testdata", 364 "gadget_tree_piboot", "meta", "gadget.yaml") 365 // ensure unpack exists 366 err = os.MkdirAll(filepath.Join(stateMachine.tempDirs.unpack, "gadget"), 0755) 367 asserter.AssertErrNil(err, true) 368 err = stateMachine.loadGadgetYaml() 369 asserter.AssertErrNil(err, true) 370 371 // populate unpack 372 files, err := os.ReadDir(filepath.Join("testdata", "gadget_tree_piboot")) 373 asserter.AssertErrNil(err, true) 374 for _, srcFile := range files { 375 srcFile := filepath.Join("testdata", "gadget_tree_piboot", srcFile.Name()) 376 err = osutilCopySpecialFile(srcFile, filepath.Join(stateMachine.tempDirs.unpack, "gadget")) 377 asserter.AssertErrNil(err, true) 378 } 379 380 // ensure volumes exists 381 err = os.MkdirAll(stateMachine.tempDirs.volumes, 0755) 382 asserter.AssertErrNil(err, true) 383 err = stateMachine.populateBootfsContents() 384 asserter.AssertErrNil(err, true) 385 386 // check that bootfs contents were actually populated 387 bootFiles := []string{"config.txt", "cmdline.txt"} 388 for _, file := range bootFiles { 389 fullPath := filepath.Join(stateMachine.stateMachineFlags.WorkDir, 390 "root", file) 391 if _, err := os.Stat(fullPath); err != nil { 392 t.Errorf("Expected %s to exist, but it does not", fullPath) 393 } 394 } 395 } 396 397 // TestFailedPopulateBootfsContents tests failures in the populateBootfsContents state 398 func TestFailedPopulateBootfsContents(t *testing.T) { 399 asserter := helper.Asserter{T: t} 400 var stateMachine StateMachine 401 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 402 403 err := stateMachine.makeTemporaryDirectories() 404 asserter.AssertErrNil(err, true) 405 t.Cleanup(func() { os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) }) 406 407 // set a valid yaml file and load it in 408 stateMachine.YamlFilePath = filepath.Join("testdata", "gadget-seed.yaml") 409 // ensure unpack exists 410 err = os.MkdirAll(filepath.Join(stateMachine.tempDirs.unpack, "gadget"), 0755) 411 asserter.AssertErrNil(err, true) 412 err = stateMachine.loadGadgetYaml() 413 asserter.AssertErrNil(err, true) 414 415 // ensure volumes exists 416 err = os.MkdirAll(stateMachine.tempDirs.volumes, 0755) 417 asserter.AssertErrNil(err, true) 418 419 // populate unpack 420 files, err := os.ReadDir(filepath.Join("testdata", "gadget_tree")) 421 asserter.AssertErrNil(err, true) 422 for _, srcFile := range files { 423 srcFile := filepath.Join("testdata", "gadget_tree", srcFile.Name()) 424 err = osutilCopySpecialFile(srcFile, filepath.Join(stateMachine.tempDirs.unpack, "gadget")) 425 asserter.AssertErrNil(err, true) 426 } 427 428 // mock gadget.LayoutVolume 429 gadgetLayoutVolume = mockLayoutVolume 430 defer func() { 431 gadgetLayoutVolume = gadget.LayoutVolume 432 }() 433 err = stateMachine.populateBootfsContents() 434 asserter.AssertErrContains(err, "Error laying out bootfs contents") 435 gadgetLayoutVolume = gadget.LayoutVolume 436 437 // mock gadget.NewMountedFilesystemWriter 438 gadgetNewMountedFilesystemWriter = mockNewMountedFilesystemWriter 439 defer func() { 440 gadgetNewMountedFilesystemWriter = gadget.NewMountedFilesystemWriter 441 }() 442 err = stateMachine.populateBootfsContents() 443 asserter.AssertErrContains(err, "Error creating NewMountedFilesystemWriter") 444 gadgetNewMountedFilesystemWriter = gadget.NewMountedFilesystemWriter 445 446 // set rootfs to an empty string in order to trigger a failure in Write() 447 oldRootfs := stateMachine.tempDirs.rootfs 448 stateMachine.tempDirs.rootfs = "" 449 err = stateMachine.populateBootfsContents() 450 asserter.AssertErrContains(err, "Error in mountedFilesystem.Write") 451 // restore rootfs 452 stateMachine.tempDirs.rootfs = oldRootfs 453 454 // cause a failure in handleSecureBoot. First change to un-seeded yaml file and load it in 455 stateMachine.YamlFilePath = filepath.Join("testdata", 456 "gadget_tree", "meta", "gadget.yaml") 457 // ensure unpack exists 458 err = stateMachine.loadGadgetYaml() 459 asserter.AssertErrNil(err, true) 460 stateMachine.IsSeeded = false 461 // now ensure grub dir exists 462 err = os.MkdirAll(filepath.Join(stateMachine.tempDirs.unpack, 463 "image", "boot", "grub"), 0755) 464 asserter.AssertErrNil(err, true) 465 // mock os.MkdirAll 466 osMkdirAll = mockMkdirAll 467 defer func() { 468 osMkdirAll = os.MkdirAll 469 }() 470 err = stateMachine.populateBootfsContents() 471 asserter.AssertErrContains(err, "Error creating ubuntu dir") 472 osMkdirAll = os.MkdirAll 473 } 474 475 // TestPopulatePreparePartitions tests a successful run of the populatePreparePartitions state 476 // and ensures that the appropriate .img files are created. It also tests that sizes smaller than 477 // the rootfs size are corrected 478 func TestPopulatePreparePartitions(t *testing.T) { 479 asserter := helper.Asserter{T: t} 480 var stateMachine StateMachine 481 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 482 483 err := stateMachine.makeTemporaryDirectories() 484 asserter.AssertErrNil(err, true) 485 t.Cleanup(func() { os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) }) 486 487 // set a valid yaml file and load it in 488 stateMachine.YamlFilePath = filepath.Join("testdata", 489 "gadget_tree", "meta", "gadget.yaml") 490 // ensure unpack exists 491 err = os.MkdirAll(filepath.Join(stateMachine.tempDirs.unpack, "gadget"), 0755) 492 asserter.AssertErrNil(err, true) 493 err = stateMachine.loadGadgetYaml() 494 asserter.AssertErrNil(err, true) 495 496 // ensure volumes exists 497 err = os.MkdirAll(stateMachine.tempDirs.volumes, 0755) 498 asserter.AssertErrNil(err, true) 499 500 // populate unpack 501 files, err := os.ReadDir(filepath.Join("testdata", "gadget_tree")) 502 asserter.AssertErrNil(err, true) 503 for _, srcFile := range files { 504 srcFile := filepath.Join("testdata", "gadget_tree", srcFile.Name()) 505 err = osutilCopySpecialFile(srcFile, filepath.Join(stateMachine.tempDirs.unpack, "gadget")) 506 asserter.AssertErrNil(err, true) 507 } 508 509 // populate bootfs contents to ensure no failures there 510 err = stateMachine.populateBootfsContents() 511 asserter.AssertErrNil(err, true) 512 513 // calculate rootfs size so the partition sizes can be set correctly 514 err = stateMachine.calculateRootfsSize() 515 asserter.AssertErrNil(err, true) 516 517 err = stateMachine.populatePreparePartitions() 518 asserter.AssertErrNil(err, true) 519 520 // ensure the .img files were created 521 for ii := 0; ii < 4; ii++ { 522 partImg := filepath.Join(stateMachine.tempDirs.volumes, 523 "pc", "part"+strconv.Itoa(ii)+".img") 524 if _, err := os.Stat(partImg); err != nil { 525 t.Errorf("File %s should exist, but does not", partImg) 526 } 527 } 528 529 // check the contents of part0.img 530 partImg := filepath.Join(stateMachine.tempDirs.volumes, 531 "pc", "part0.img") 532 partImgBytes, err := os.ReadFile(partImg) 533 asserter.AssertErrNil(err, true) 534 dataBytes := make([]byte, 440) 535 // partImg should consist of these 11 bytes and 429 null bytes 536 copy(dataBytes[:11], []byte{84, 69, 83, 84, 32, 70, 73, 76, 69, 10}) 537 if !bytes.Equal(partImgBytes, dataBytes) { 538 t.Errorf("Expected part0.img to contain %v, instead got %v %d", 539 dataBytes, partImgBytes, len(partImgBytes)) 540 } 541 } 542 543 // TestFailedPopulatePreparePartitions tests failures in the populatePreparePartitions state 544 func TestFailedPopulatePreparePartitions(t *testing.T) { 545 asserter := helper.Asserter{T: t} 546 var stateMachine StateMachine 547 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 548 549 err := stateMachine.makeTemporaryDirectories() 550 asserter.AssertErrNil(err, true) 551 t.Cleanup(func() { os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) }) 552 553 // set a valid yaml file and load it in 554 stateMachine.YamlFilePath = filepath.Join("testdata", 555 "gadget_tree", "meta", "gadget.yaml") 556 // ensure unpack exists 557 err = os.MkdirAll(filepath.Join(stateMachine.tempDirs.unpack, "gadget"), 0755) 558 asserter.AssertErrNil(err, true) 559 err = stateMachine.loadGadgetYaml() 560 asserter.AssertErrNil(err, true) 561 562 // ensure volumes exists 563 err = os.MkdirAll(stateMachine.tempDirs.volumes, 0755) 564 asserter.AssertErrNil(err, true) 565 566 // populate unpack 567 files, err := os.ReadDir(filepath.Join("testdata", "gadget_tree")) 568 asserter.AssertErrNil(err, true) 569 for _, srcFile := range files { 570 srcFile := filepath.Join("testdata", "gadget_tree", srcFile.Name()) 571 err = osutilCopySpecialFile(srcFile, filepath.Join(stateMachine.tempDirs.unpack, "gadget")) 572 asserter.AssertErrNil(err, true) 573 } 574 575 // populate bootfs contents to ensure no failures there 576 err = stateMachine.populateBootfsContents() 577 asserter.AssertErrNil(err, true) 578 579 // now mock helper.CopyBlob to cause an error in copyStructureContent 580 helperCopyBlob = mockCopyBlob 581 defer func() { 582 helperCopyBlob = helper.CopyBlob 583 }() 584 err = stateMachine.populatePreparePartitions() 585 asserter.AssertErrContains(err, "Error zeroing partition") 586 helperCopyBlob = helper.CopyBlob 587 588 // set a bootloader to lk and mock mkdir to cause a failure in that function 589 for _, volume := range stateMachine.GadgetInfo.Volumes { 590 volume.Bootloader = "lk" 591 } 592 osMkdir = mockMkdir 593 defer func() { 594 osMkdir = os.Mkdir 595 }() 596 err = stateMachine.populatePreparePartitions() 597 asserter.AssertErrContains(err, "got lk bootloader but directory") 598 osMkdir = os.Mkdir 599 } 600 601 // TestEmptyPartPopulatePreparePartitions performs a successful run a gadget.yaml that has, 602 // besides regular partitions, one empty partition and makes sure that a partition image file 603 // has been created for it (LP: #1947863) 604 func TestEmptyPartPopulatePreparePartitions(t *testing.T) { 605 asserter := helper.Asserter{T: t} 606 var stateMachine StateMachine 607 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 608 609 err := stateMachine.makeTemporaryDirectories() 610 asserter.AssertErrNil(err, true) 611 t.Cleanup(func() { os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) }) 612 613 // set a valid yaml file and load it in 614 // we use a special gadget.yaml here, special for this testcase 615 stateMachine.YamlFilePath = filepath.Join("testdata", 616 "gadget-empty-part.yaml") 617 // ensure unpack exists 618 err = os.MkdirAll(filepath.Join(stateMachine.tempDirs.unpack, "gadget"), 0755) 619 asserter.AssertErrNil(err, true) 620 err = stateMachine.loadGadgetYaml() 621 asserter.AssertErrNil(err, true) 622 623 // ensure volumes exists 624 err = os.MkdirAll(stateMachine.tempDirs.volumes, 0755) 625 asserter.AssertErrNil(err, true) 626 627 // populate unpack 628 files, err := os.ReadDir(filepath.Join("testdata", "gadget_tree")) 629 asserter.AssertErrNil(err, true) 630 for _, srcFile := range files { 631 srcFile := filepath.Join("testdata", "gadget_tree", srcFile.Name()) 632 err = osutilCopySpecialFile(srcFile, filepath.Join(stateMachine.tempDirs.unpack, "gadget")) 633 asserter.AssertErrNil(err, true) 634 } 635 636 // populate bootfs contents to ensure no failures there 637 err = stateMachine.populateBootfsContents() 638 asserter.AssertErrNil(err, true) 639 640 // calculate rootfs size so the partition sizes can be set correctly 641 err = stateMachine.calculateRootfsSize() 642 asserter.AssertErrNil(err, true) 643 644 err = stateMachine.populatePreparePartitions() 645 asserter.AssertErrNil(err, true) 646 647 // ensure the .img files were created 648 for ii := 0; ii < 5; ii++ { 649 partImg := filepath.Join(stateMachine.tempDirs.volumes, 650 "pc", "part"+strconv.Itoa(ii)+".img") 651 if _, err := os.Stat(partImg); err != nil { 652 t.Errorf("File %s should exist, but does not", partImg) 653 } 654 } 655 656 // check part2.img, it should be empty and have a 4K size 657 partImg := filepath.Join(stateMachine.tempDirs.volumes, 658 "pc", "part2.img") 659 partImgBytes, err := os.ReadFile(partImg) 660 asserter.AssertErrNil(err, true) 661 // these are all zeroes 662 dataBytes := make([]byte, 4096) 663 if !bytes.Equal(partImgBytes, dataBytes) { 664 t.Errorf("Expected part2.img to contain %d zeroes, got something different (size %d)", 665 len(dataBytes), len(partImgBytes)) 666 } 667 } 668 669 // TestMakeDiskPartitionSchemes tests that makeDisk() can successfully parse 670 // mbr, gpt, and hybrid schemes. It then runs "dumpe2fs" to ensure the 671 // resulting disk has the correct type of partition table. 672 // We also check various sector sizes while at it and rootfs placements 673 func TestMakeDiskPartitionSchemes(t *testing.T) { 674 testCases := []struct { 675 name string 676 tableType string 677 sectorSize string 678 rootfsVolName string 679 rootfsPartNum int 680 }{ 681 {"gpt", "gpt", "512", "pc", 3}, 682 {"mbr", "dos", "512", "pc", 3}, 683 {"hybrid", "gpt", "512", "pc", 3}, 684 {"gpt4k", "PMBR", "4096", "pc", 3}, // PMBR still seems valid GPT 685 {"gpt-efi-only", "gpt", "512", "pc", 2}, 686 } 687 for _, tc := range testCases { 688 t.Run("test_make_disk_partition_type_"+tc.name, func(t *testing.T) { 689 asserter := helper.Asserter{T: t} 690 var stateMachine StateMachine 691 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 692 693 // set the sector size to the one needed during testing 694 stateMachine.commonFlags.SectorSize = tc.sectorSize 695 696 err := stateMachine.makeTemporaryDirectories() 697 asserter.AssertErrNil(err, true) 698 t.Cleanup(func() { os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) }) 699 700 // also set up an output directory 701 outDir, err := os.MkdirTemp("/tmp", "ubuntu-image-") 702 asserter.AssertErrNil(err, true) 703 t.Cleanup(func() { os.RemoveAll(outDir) }) 704 stateMachine.commonFlags.OutputDir = outDir 705 706 // set up volume names 707 stateMachine.VolumeNames = map[string]string{ 708 "pc": "pc.img", 709 } 710 711 // set a valid yaml file and load it in 712 stateMachine.YamlFilePath = filepath.Join("testdata", 713 "gadget-"+tc.name+".yaml") 714 // ensure unpack exists 715 err = os.MkdirAll(filepath.Join(stateMachine.tempDirs.unpack, "gadget"), 0755) 716 asserter.AssertErrNil(err, true) 717 err = stateMachine.loadGadgetYaml() 718 asserter.AssertErrNil(err, true) 719 720 // set up a "rootfs" that we can eventually copy into the disk 721 err = os.MkdirAll(stateMachine.tempDirs.rootfs, 0755) 722 asserter.AssertErrNil(err, true) 723 err = osutil.CopySpecialFile(filepath.Join("testdata", "gadget_tree"), stateMachine.tempDirs.rootfs) 724 asserter.AssertErrNil(err, true) 725 726 // also need to set the rootfs size to avoid partition errors 727 err = stateMachine.calculateRootfsSize() 728 asserter.AssertErrNil(err, true) 729 730 // ensure volumes exists 731 err = os.MkdirAll(stateMachine.tempDirs.volumes, 0755) 732 asserter.AssertErrNil(err, true) 733 734 // populate unpack 735 files, err := os.ReadDir(filepath.Join("testdata", "gadget_tree")) 736 asserter.AssertErrNil(err, true) 737 for _, srcFile := range files { 738 srcFile := filepath.Join("testdata", "gadget_tree", srcFile.Name()) 739 err = osutil.CopySpecialFile(srcFile, filepath.Join(stateMachine.tempDirs.unpack, "gadget")) 740 asserter.AssertErrNil(err, true) 741 } 742 743 // run through the rest of the states 744 err = stateMachine.populateBootfsContents() 745 asserter.AssertErrNil(err, true) 746 747 err = stateMachine.populatePreparePartitions() 748 asserter.AssertErrNil(err, true) 749 750 err = stateMachine.makeDisk() 751 asserter.AssertErrNil(err, true) 752 753 // now run "dumpe2fs" to ensure the correct type of partition table exists 754 imgFile := filepath.Join(stateMachine.commonFlags.OutputDir, "pc.img") 755 dumpe2fsCommand := *exec.Command("dumpe2fs", imgFile) 756 757 dumpe2fsBytes, _ := dumpe2fsCommand.CombinedOutput() // nolint: errcheck 758 // The command will return an error because the image itself is not valid but we do 759 // not care here. 760 if !strings.Contains(string(dumpe2fsBytes), tc.tableType) { 761 t.Errorf("File %s should have partition table %s, instead got \"%s\"", 762 imgFile, tc.tableType, string(dumpe2fsBytes)) 763 } 764 765 // ensure the resulting image file is a multiple of the block size 766 diskImg, err := diskfs.Open(imgFile) 767 asserter.AssertErrNil(err, true) 768 defer diskImg.File.Close() 769 if diskImg.Size%int64(stateMachine.SectorSize) != 0 { 770 t.Errorf("Disk image size %d is not an multiple of the block size: %d", 771 diskImg.Size, int64(stateMachine.SectorSize)) 772 } 773 774 // while at it, ensure that the root partition has been found 775 if stateMachine.RootfsPartNum != tc.rootfsPartNum || stateMachine.RootfsVolName != tc.rootfsVolName { 776 t.Errorf("Root partition volume/numbe not detected correctly, expected %s/%d, got %s/%d", 777 tc.rootfsVolName, tc.rootfsPartNum, stateMachine.RootfsVolName, stateMachine.RootfsPartNum) 778 } 779 }) 780 } 781 } 782 783 // TestFailedMakeDisk tests failures in the MakeDisk state 784 func TestFailedMakeDisk(t *testing.T) { 785 asserter := helper.Asserter{T: t} 786 var stateMachine StateMachine 787 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 788 789 err := stateMachine.makeTemporaryDirectories() 790 asserter.AssertErrNil(err, true) 791 t.Cleanup(func() { os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) }) 792 793 // also set up an output directory 794 outDir, err := os.MkdirTemp("/tmp", "ubuntu-image-") 795 asserter.AssertErrNil(err, true) 796 t.Cleanup(func() { os.RemoveAll(outDir) }) 797 stateMachine.commonFlags.OutputDir = outDir 798 err = stateMachine.determineOutputDirectory() 799 asserter.AssertErrNil(err, true) 800 801 // set up volume names 802 stateMachine.VolumeNames = map[string]string{ 803 "pc": "pc.img", 804 } 805 806 // set a valid yaml file and load it in 807 stateMachine.YamlFilePath = filepath.Join("testdata", "gadget-mbr.yaml") 808 // ensure unpack exists 809 err = os.MkdirAll(filepath.Join(stateMachine.tempDirs.unpack, "gadget"), 0755) 810 asserter.AssertErrNil(err, true) 811 err = stateMachine.loadGadgetYaml() 812 asserter.AssertErrNil(err, true) 813 814 // also need to set the rootfs size to avoid partition errors 815 err = stateMachine.calculateRootfsSize() 816 asserter.AssertErrNil(err, true) 817 818 // ensure volumes exists 819 err = os.MkdirAll(stateMachine.tempDirs.volumes, 0755) 820 asserter.AssertErrNil(err, true) 821 822 // populate unpack 823 files, err := os.ReadDir(filepath.Join("testdata", "gadget_tree")) 824 asserter.AssertErrNil(err, true) 825 for _, srcFile := range files { 826 srcFile := filepath.Join("testdata", "gadget_tree", srcFile.Name()) 827 err = osutilCopySpecialFile(srcFile, filepath.Join(stateMachine.tempDirs.unpack, "gadget")) 828 asserter.AssertErrNil(err, true) 829 } 830 831 // mock os.RemoveAll 832 osRemoveAll = mockRemoveAll 833 defer func() { 834 osRemoveAll = os.RemoveAll 835 }() 836 err = stateMachine.makeDisk() 837 asserter.AssertErrContains(err, "Error removing old disk image") 838 osRemoveAll = os.RemoveAll 839 840 // mock diskfs.Create 841 diskfsCreate = mockDiskfsCreate 842 defer func() { 843 diskfsCreate = diskfs.Create 844 }() 845 err = stateMachine.makeDisk() 846 asserter.AssertErrContains(err, "Error creating disk image") 847 diskfsCreate = diskfs.Create 848 849 // mock os.Truncate 850 osTruncate = mockTruncate 851 defer func() { 852 osTruncate = os.Truncate 853 }() 854 err = stateMachine.makeDisk() 855 asserter.AssertErrContains(err, "Error resizing disk image") 856 osTruncate = os.Truncate 857 858 // mock diskfs.Create to create a read only disk 859 diskfsCreate = readOnlyDiskfsCreate 860 defer func() { 861 diskfsCreate = diskfs.Create 862 }() 863 err = stateMachine.makeDisk() 864 asserter.AssertErrContains(err, "Error partitioning image file") 865 diskfsCreate = diskfs.Create 866 867 // mock os.OpenFile 868 // errors in file.WriteAt() 869 osOpenFile = mockOpenFile 870 defer func() { 871 osOpenFile = os.OpenFile 872 }() 873 err = stateMachine.makeDisk() 874 asserter.AssertErrContains(err, "Error opening disk to write MBR disk identifier") 875 osOpenFile = os.OpenFile 876 877 // mock rand.Read 878 // errors in generateUniqueDiskID() 879 randRead = mockRandRead 880 defer func() { 881 randRead = rand.Read 882 }() 883 err = stateMachine.makeDisk() 884 asserter.AssertErrContains(err, "Error generating disk ID") 885 randRead = rand.Read 886 887 // mock os.OpenFile to force it to use os.O_APPEND, which causes 888 // errors in file.WriteAt() 889 osOpenFile = mockOpenFileAppend 890 defer func() { 891 osOpenFile = os.OpenFile 892 }() 893 err = stateMachine.makeDisk() 894 asserter.AssertErrContains(err, "Error writing MBR disk identifier") 895 osOpenFile = os.OpenFile 896 897 // mock helper.CopyBlob to simulate a failure in copyDataToImage 898 helperCopyBlob = mockCopyBlob 899 defer func() { 900 helperCopyBlob = helper.CopyBlob 901 }() 902 err = stateMachine.makeDisk() 903 asserter.AssertErrContains(err, "Error writing disk image") 904 helperCopyBlob = helper.CopyBlob 905 906 // Change to GPT for these next tests 907 stateMachine.YamlFilePath = filepath.Join("testdata", "gadget-gpt.yaml") 908 err = stateMachine.loadGadgetYaml() 909 asserter.AssertErrNil(err, true) 910 911 err = stateMachine.populateBootfsContents() 912 asserter.AssertErrNil(err, true) 913 914 err = stateMachine.populatePreparePartitions() 915 asserter.AssertErrNil(err, true) 916 917 // mock os.OpenFile to simulate a failure in writeOffsetValues 918 osOpenFile = mockOpenFile 919 defer func() { 920 osOpenFile = os.OpenFile 921 }() 922 // also mock helperCopyBlob to ignore missing files and return success 923 helperCopyBlob = mockCopyBlobSuccess 924 defer func() { 925 helperCopyBlob = helper.CopyBlob 926 }() 927 err = stateMachine.makeDisk() 928 asserter.AssertErrContains(err, "Error opening image file") 929 osOpenFile = os.OpenFile 930 helperCopyBlob = helper.CopyBlob 931 932 helperCopyBlob = mockCopyBlob 933 defer func() { 934 helperCopyBlob = helper.CopyBlob 935 }() 936 stateMachine.cleanWorkDir = true // for coverage! 937 stateMachine.commonFlags.OutputDir = "" 938 defer os.Remove("pc.img") 939 err = stateMachine.makeDisk() 940 asserter.AssertErrContains(err, "Error writing disk image") 941 helperCopyBlob = helper.CopyBlob 942 943 // make sure with no OutputDir the image was created in the cwd 944 _, err = os.Stat("pc.img") 945 asserter.AssertErrNil(err, true) 946 } 947 948 // TestImageSizeFlag performs a successful call to StateMachine.MakeDisk with the 949 // --image-size flag, and ensures that the resulting image is the size specified 950 // with the flag (LP: #1947867) 951 func TestImageSizeFlag(t *testing.T) { 952 testCases := []struct { 953 name string 954 sizeArg string 955 gadgetTree string 956 imageSize map[string]quantity.Size 957 volNames map[string]string 958 }{ 959 { 960 "one_volume", 961 "4G", 962 filepath.Join("testdata", "gadget_tree"), 963 map[string]quantity.Size{ 964 "pc": 4 * quantity.SizeGiB, 965 }, 966 map[string]string{ 967 "pc": "pc.img", 968 }, 969 }, 970 { 971 "multi_volume", 972 "first:4G,second:1G", 973 filepath.Join("testdata", "gadget_tree_multi"), 974 map[string]quantity.Size{ 975 "first": 4 * quantity.SizeGiB, 976 "second": 1 * quantity.SizeGiB, 977 }, 978 map[string]string{ 979 "first": "first.img", 980 "second": "second.img", 981 }, 982 }, 983 } 984 for _, tc := range testCases { 985 t.Run(tc.name, func(t *testing.T) { 986 asserter := helper.Asserter{T: t} 987 var stateMachine StateMachine 988 stateMachine.commonFlags, stateMachine.stateMachineFlags = helper.InitCommonOpts() 989 stateMachine.IsSeeded = true 990 stateMachine.commonFlags.Size = tc.sizeArg 991 992 err := stateMachine.makeTemporaryDirectories() 993 asserter.AssertErrNil(err, true) 994 //t.Cleanup(func() { os.RemoveAll(stateMachine.stateMachineFlags.WorkDir) }) 995 996 // also set up an output directory 997 outDir, err := os.MkdirTemp("/tmp", "ubuntu-image-") 998 asserter.AssertErrNil(err, true) 999 //t.Cleanup(func() { os.RemoveAll(outDir) }) 1000 stateMachine.commonFlags.OutputDir = outDir 1001 1002 // set up volume names 1003 stateMachine.VolumeNames = tc.volNames 1004 1005 // set up a "rootfs" that we can eventually copy into the disk 1006 err = os.MkdirAll(stateMachine.tempDirs.rootfs, 0755) 1007 asserter.AssertErrNil(err, true) 1008 err = osutil.CopySpecialFile(tc.gadgetTree, stateMachine.tempDirs.rootfs) 1009 asserter.AssertErrNil(err, true) 1010 1011 // set a valid yaml file and load it in 1012 stateMachine.YamlFilePath = filepath.Join(tc.gadgetTree, "meta", "gadget.yaml") 1013 // ensure unpack exists 1014 err = os.MkdirAll(filepath.Join(stateMachine.tempDirs.unpack, "gadget"), 0755) 1015 asserter.AssertErrNil(err, true) 1016 err = stateMachine.loadGadgetYaml() 1017 asserter.AssertErrNil(err, true) 1018 1019 // ensure volumes exists 1020 err = os.MkdirAll(stateMachine.tempDirs.volumes, 0755) 1021 asserter.AssertErrNil(err, true) 1022 // populate unpack 1023 files, err := os.ReadDir(tc.gadgetTree) 1024 asserter.AssertErrNil(err, true) 1025 for _, srcFile := range files { 1026 srcFile := filepath.Join(tc.gadgetTree, srcFile.Name()) 1027 err = osutil.CopySpecialFile(srcFile, filepath.Join(stateMachine.tempDirs.unpack, "gadget")) 1028 asserter.AssertErrNil(err, true) 1029 } 1030 1031 // also need to set the rootfs size to avoid partition errors 1032 err = stateMachine.calculateRootfsSize() 1033 asserter.AssertErrNil(err, true) 1034 1035 // run through the rest of the states 1036 err = stateMachine.populateBootfsContents() 1037 asserter.AssertErrNil(err, true) 1038 1039 err = stateMachine.populatePreparePartitions() 1040 asserter.AssertErrNil(err, true) 1041 1042 err = stateMachine.makeDisk() 1043 asserter.AssertErrNil(err, true) 1044 1045 // check the size of the disk(s) 1046 for volume, expectedSize := range tc.imageSize { 1047 imgFile := filepath.Join(stateMachine.commonFlags.OutputDir, volume+".img") 1048 diskImg, err := os.Stat(imgFile) 1049 asserter.AssertErrNil(err, true) 1050 if diskImg.Size() != int64(expectedSize) { 1051 t.Errorf("--image-size %d was specified, but resulting image is %d bytes", 1052 expectedSize, diskImg.Size()) 1053 } 1054 } 1055 }) 1056 1057 } 1058 }