github.com/kubiko/snapd@v0.0.0-20201013125620-d4f3094d9ddf/gadget/mountedfilesystem_test.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2019 Canonical Ltd 5 * 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License version 3 as 8 * published by the Free Software Foundation. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 package gadget_test 21 22 import ( 23 "errors" 24 "fmt" 25 "os" 26 "path/filepath" 27 "strings" 28 29 . "gopkg.in/check.v1" 30 31 "github.com/snapcore/snapd/gadget" 32 "github.com/snapcore/snapd/osutil" 33 "github.com/snapcore/snapd/strutil" 34 "github.com/snapcore/snapd/testutil" 35 ) 36 37 type mountedfilesystemTestSuite struct { 38 dir string 39 backup string 40 } 41 42 var _ = Suite(&mountedfilesystemTestSuite{}) 43 44 func (s *mountedfilesystemTestSuite) SetUpTest(c *C) { 45 s.dir = c.MkDir() 46 s.backup = c.MkDir() 47 } 48 49 type gadgetData struct { 50 name, target, symlinkTo, content string 51 } 52 53 func makeGadgetData(c *C, where string, data []gadgetData) { 54 for _, en := range data { 55 if en.name == "" { 56 continue 57 } 58 if strings.HasSuffix(en.name, "/") { 59 err := os.MkdirAll(filepath.Join(where, en.name), 0755) 60 c.Check(en.content, HasLen, 0) 61 c.Assert(err, IsNil) 62 continue 63 } 64 if en.symlinkTo != "" { 65 err := os.Symlink(en.symlinkTo, filepath.Join(where, en.name)) 66 c.Assert(err, IsNil) 67 continue 68 } 69 makeSizedFile(c, filepath.Join(where, en.name), 0, []byte(en.content)) 70 } 71 } 72 73 func verifyWrittenGadgetData(c *C, where string, data []gadgetData) { 74 for _, en := range data { 75 if en.target == "" { 76 continue 77 } 78 if en.symlinkTo != "" { 79 symlinkTarget, err := os.Readlink(filepath.Join(where, en.target)) 80 c.Assert(err, IsNil) 81 c.Check(symlinkTarget, Equals, en.symlinkTo) 82 continue 83 } 84 target := filepath.Join(where, en.target) 85 c.Check(target, testutil.FileContains, en.content) 86 } 87 } 88 89 func makeExistingData(c *C, where string, data []gadgetData) { 90 for _, en := range data { 91 if en.target == "" { 92 continue 93 } 94 if strings.HasSuffix(en.target, "/") { 95 err := os.MkdirAll(filepath.Join(where, en.target), 0755) 96 c.Check(en.content, HasLen, 0) 97 c.Assert(err, IsNil) 98 continue 99 } 100 if en.symlinkTo != "" { 101 err := os.Symlink(en.symlinkTo, filepath.Join(where, en.target)) 102 c.Assert(err, IsNil) 103 continue 104 } 105 makeSizedFile(c, filepath.Join(where, en.target), 0, []byte(en.content)) 106 } 107 } 108 109 type contentType int 110 111 const ( 112 typeFile contentType = iota 113 typeDir 114 ) 115 116 func verifyDirContents(c *C, where string, expected map[string]contentType) { 117 cleanWhere := filepath.Clean(where) 118 119 got := make(map[string]contentType) 120 err := filepath.Walk(where, func(name string, fi os.FileInfo, err error) error { 121 if err != nil { 122 return err 123 } 124 if name == where { 125 return nil 126 } 127 suffixName := name[len(cleanWhere)+1:] 128 t := typeFile 129 if fi.IsDir() { 130 t = typeDir 131 } 132 got[suffixName] = t 133 134 for prefix := filepath.Dir(name); prefix != where; prefix = filepath.Dir(prefix) { 135 delete(got, prefix[len(cleanWhere)+1:]) 136 } 137 138 return nil 139 }) 140 c.Assert(err, IsNil) 141 if len(expected) > 0 { 142 c.Assert(got, DeepEquals, expected) 143 } else { 144 c.Assert(got, HasLen, 0) 145 } 146 } 147 148 func (s *mountedfilesystemTestSuite) TestWriteFile(c *C) { 149 makeSizedFile(c, filepath.Join(s.dir, "foo"), 0, []byte("foo foo foo")) 150 151 outDir := c.MkDir() 152 153 // foo -> /foo 154 err := gadget.WriteFile(filepath.Join(s.dir, "foo"), filepath.Join(outDir, "foo"), nil) 155 c.Assert(err, IsNil) 156 c.Check(filepath.Join(outDir, "foo"), testutil.FileEquals, []byte("foo foo foo")) 157 158 // foo -> bar/foo 159 err = gadget.WriteFile(filepath.Join(s.dir, "foo"), filepath.Join(outDir, "bar/foo"), nil) 160 c.Assert(err, IsNil) 161 c.Check(filepath.Join(outDir, "bar/foo"), testutil.FileEquals, []byte("foo foo foo")) 162 163 // overwrites 164 makeSizedFile(c, filepath.Join(outDir, "overwrite"), 0, []byte("disappear")) 165 err = gadget.WriteFile(filepath.Join(s.dir, "foo"), filepath.Join(outDir, "overwrite"), nil) 166 c.Assert(err, IsNil) 167 c.Check(filepath.Join(outDir, "overwrite"), testutil.FileEquals, []byte("foo foo foo")) 168 169 // unless told to preserve 170 keepName := filepath.Join(outDir, "keep") 171 makeSizedFile(c, keepName, 0, []byte("can't touch this")) 172 err = gadget.WriteFile(filepath.Join(s.dir, "foo"), filepath.Join(outDir, "keep"), []string{keepName}) 173 c.Assert(err, IsNil) 174 c.Check(filepath.Join(outDir, "keep"), testutil.FileEquals, []byte("can't touch this")) 175 176 err = gadget.WriteFile(filepath.Join(s.dir, "not-found"), filepath.Join(outDir, "foo"), nil) 177 c.Assert(err, ErrorMatches, "cannot copy .*: unable to open .*/not-found: .* no such file or directory") 178 } 179 180 func (s *mountedfilesystemTestSuite) TestWriteDirectoryContents(c *C) { 181 gd := []gadgetData{ 182 {name: "boot-assets/splash", target: "splash", content: "splash"}, 183 {name: "boot-assets/some-dir/data", target: "some-dir/data", content: "data"}, 184 {name: "boot-assets/some-dir/empty-file", target: "some-dir/empty-file"}, 185 {name: "boot-assets/nested-dir/nested", target: "/nested-dir/nested", content: "nested"}, 186 {name: "boot-assets/nested-dir/more-nested/more", target: "/nested-dir/more-nested/more", content: "more"}, 187 } 188 makeGadgetData(c, s.dir, gd) 189 190 ps := &gadget.LaidOutStructure{ 191 VolumeStructure: &gadget.VolumeStructure{ 192 Filesystem: "ext4", 193 }, 194 } 195 rw, err := gadget.NewMountedFilesystemWriter(s.dir, ps, nil) 196 c.Assert(err, IsNil) 197 198 outDir := c.MkDir() 199 // boot-assets/ -> / (contents of boot assets under /) 200 err = rw.WriteDirectory(outDir, filepath.Join(s.dir, "boot-assets")+"/", outDir+"/", nil) 201 c.Assert(err, IsNil) 202 203 verifyWrittenGadgetData(c, outDir, gd) 204 } 205 206 func (s *mountedfilesystemTestSuite) TestWriteDirectoryWhole(c *C) { 207 gd := []gadgetData{ 208 {name: "boot-assets/splash", target: "boot-assets/splash", content: "splash"}, 209 {name: "boot-assets/some-dir/data", target: "boot-assets/some-dir/data", content: "data"}, 210 {name: "boot-assets/some-dir/empty-file", target: "boot-assets/some-dir/empty-file"}, 211 {name: "boot-assets/nested-dir/nested", target: "boot-assets/nested-dir/nested", content: "nested"}, 212 {name: "boot-assets/nested-dir/more-nested/more", target: "boot-assets//nested-dir/more-nested/more", content: "more"}, 213 } 214 makeGadgetData(c, s.dir, gd) 215 216 ps := &gadget.LaidOutStructure{ 217 VolumeStructure: &gadget.VolumeStructure{ 218 Filesystem: "ext4", 219 }, 220 } 221 rw, err := gadget.NewMountedFilesystemWriter(s.dir, ps, nil) 222 c.Assert(err, IsNil) 223 224 outDir := c.MkDir() 225 // boot-assets -> / (boot-assets and children under /) 226 err = rw.WriteDirectory(outDir, filepath.Join(s.dir, "boot-assets"), outDir+"/", nil) 227 c.Assert(err, IsNil) 228 229 verifyWrittenGadgetData(c, outDir, gd) 230 } 231 232 func (s *mountedfilesystemTestSuite) TestWriteNonDirectory(c *C) { 233 gd := []gadgetData{ 234 {name: "foo", content: "nested"}, 235 } 236 makeGadgetData(c, s.dir, gd) 237 ps := &gadget.LaidOutStructure{ 238 VolumeStructure: &gadget.VolumeStructure{ 239 Filesystem: "ext4", 240 }, 241 } 242 rw, err := gadget.NewMountedFilesystemWriter(s.dir, ps, nil) 243 c.Assert(err, IsNil) 244 245 outDir := c.MkDir() 246 247 err = rw.WriteDirectory(outDir, filepath.Join(s.dir, "foo")+"/", outDir, nil) 248 c.Assert(err, ErrorMatches, `cannot specify trailing / for a source which is not a directory`) 249 250 err = rw.WriteDirectory(outDir, filepath.Join(s.dir, "foo"), outDir, nil) 251 c.Assert(err, ErrorMatches, `source is not a directory`) 252 } 253 254 type mockContentChange struct { 255 path string 256 change *gadget.ContentChange 257 } 258 259 type mockWriteObserver struct { 260 content map[string][]*mockContentChange 261 preserveTargets []string 262 observeErr error 263 expectedStruct *gadget.LaidOutStructure 264 c *C 265 } 266 267 func (m *mockWriteObserver) Observe(op gadget.ContentOperation, sourceStruct *gadget.LaidOutStructure, 268 targetRootDir, relativeTargetPath string, data *gadget.ContentChange) (gadget.ContentChangeAction, error) { 269 if m.c == nil { 270 panic("c is unset") 271 } 272 m.c.Assert(data, NotNil) 273 m.c.Assert(op, Equals, gadget.ContentWrite, Commentf("unexpected operation %v", op)) 274 if m.content == nil { 275 m.content = make(map[string][]*mockContentChange) 276 } 277 // the file with content that will be written must exist 278 m.c.Check(osutil.FileExists(data.After) && !osutil.IsDirectory(data.After), Equals, true, 279 Commentf("path %q does not exist or is a directory", data.After)) 280 // all files are treated as new by the writer 281 m.c.Check(data.Before, Equals, "") 282 m.c.Check(filepath.IsAbs(relativeTargetPath), Equals, false, 283 Commentf("target path %q is absolute", relativeTargetPath)) 284 285 m.content[targetRootDir] = append(m.content[targetRootDir], 286 &mockContentChange{path: relativeTargetPath, change: data}) 287 288 m.c.Assert(sourceStruct, NotNil) 289 m.c.Check(m.expectedStruct, DeepEquals, sourceStruct) 290 291 if strutil.ListContains(m.preserveTargets, relativeTargetPath) { 292 return gadget.ChangeIgnore, nil 293 } 294 return gadget.ChangeApply, m.observeErr 295 } 296 297 func (s *mountedfilesystemTestSuite) TestMountedWriterHappy(c *C) { 298 gd := []gadgetData{ 299 {name: "foo", target: "foo-dir/foo", content: "foo foo foo"}, 300 {name: "bar", target: "bar-name", content: "bar bar bar"}, 301 {name: "boot-assets/splash", target: "splash", content: "splash"}, 302 {name: "boot-assets/some-dir/data", target: "some-dir/data", content: "data"}, 303 {name: "boot-assets/some-dir/data", target: "data-copy", content: "data"}, 304 {name: "boot-assets/some-dir/empty-file", target: "some-dir/empty-file"}, 305 {name: "boot-assets/nested-dir/nested", target: "/nested-copy/nested", content: "nested"}, 306 {name: "boot-assets/nested-dir/more-nested/more", target: "/nested-copy/more-nested/more", content: "more"}, 307 {name: "baz", target: "/baz", content: "baz"}, 308 } 309 makeGadgetData(c, s.dir, gd) 310 err := os.MkdirAll(filepath.Join(s.dir, "boot-assets/empty-dir"), 0755) 311 c.Assert(err, IsNil) 312 313 ps := &gadget.LaidOutStructure{ 314 VolumeStructure: &gadget.VolumeStructure{ 315 Name: "hello", 316 Size: 2048, 317 Filesystem: "ext4", 318 Content: []gadget.VolumeContent{ 319 { 320 // single file in target directory 321 Source: "foo", 322 Target: "/foo-dir/", 323 }, { 324 // single file under different name 325 Source: "bar", 326 Target: "/bar-name", 327 }, { 328 // whole directory contents 329 Source: "boot-assets/", 330 Target: "/", 331 }, { 332 // single file from nested directory 333 Source: "boot-assets/some-dir/data", 334 Target: "/data-copy", 335 }, { 336 // contents of nested directory under new target directory 337 Source: "boot-assets/nested-dir/", 338 Target: "/nested-copy/", 339 }, { 340 // contents of nested directory under new target directory 341 Source: "baz", 342 Target: "baz", 343 }, 344 }, 345 }, 346 } 347 348 outDir := c.MkDir() 349 350 obs := &mockWriteObserver{ 351 c: c, 352 expectedStruct: ps, 353 } 354 rw, err := gadget.NewMountedFilesystemWriter(s.dir, ps, obs) 355 c.Assert(err, IsNil) 356 c.Assert(rw, NotNil) 357 358 err = rw.Write(outDir, nil) 359 c.Assert(err, IsNil) 360 361 verifyWrittenGadgetData(c, outDir, gd) 362 c.Assert(osutil.IsDirectory(filepath.Join(outDir, "empty-dir")), Equals, true) 363 364 // verify observer was notified of writes for files only 365 c.Assert(obs.content, DeepEquals, map[string][]*mockContentChange{ 366 outDir: { 367 {"foo-dir/foo", &gadget.ContentChange{After: filepath.Join(s.dir, "foo")}}, 368 {"bar-name", &gadget.ContentChange{After: filepath.Join(s.dir, "bar")}}, 369 370 {"nested-dir/more-nested/more", &gadget.ContentChange{ 371 After: filepath.Join(s.dir, "boot-assets/nested-dir/more-nested/more"), 372 }}, 373 {"nested-dir/nested", &gadget.ContentChange{ 374 After: filepath.Join(s.dir, "boot-assets/nested-dir/nested"), 375 }}, 376 {"some-dir/data", &gadget.ContentChange{ 377 After: filepath.Join(s.dir, "boot-assets/some-dir/data"), 378 }}, 379 {"some-dir/empty-file", &gadget.ContentChange{ 380 After: filepath.Join(s.dir, "boot-assets/some-dir/empty-file"), 381 }}, 382 {"splash", &gadget.ContentChange{ 383 After: filepath.Join(s.dir, "boot-assets/splash"), 384 }}, 385 386 {"data-copy", &gadget.ContentChange{ 387 After: filepath.Join(s.dir, "boot-assets/some-dir/data"), 388 }}, 389 390 {"nested-copy/more-nested/more", &gadget.ContentChange{ 391 After: filepath.Join(s.dir, "boot-assets/nested-dir/more-nested/more"), 392 }}, 393 {"nested-copy/nested", &gadget.ContentChange{ 394 After: filepath.Join(s.dir, "boot-assets/nested-dir/nested"), 395 }}, 396 397 {"baz", &gadget.ContentChange{After: filepath.Join(s.dir, "baz")}}, 398 }, 399 }) 400 } 401 402 func (s *mountedfilesystemTestSuite) TestMountedWriterNonDirectory(c *C) { 403 gd := []gadgetData{ 404 {name: "foo", content: "nested"}, 405 } 406 makeGadgetData(c, s.dir, gd) 407 408 ps := &gadget.LaidOutStructure{ 409 VolumeStructure: &gadget.VolumeStructure{ 410 Size: 2048, 411 Filesystem: "ext4", 412 Content: []gadget.VolumeContent{ 413 { 414 // contents of nested directory under new target directory 415 Source: "foo/", 416 Target: "/nested-copy/", 417 }, 418 }, 419 }, 420 } 421 422 outDir := c.MkDir() 423 424 rw, err := gadget.NewMountedFilesystemWriter(s.dir, ps, nil) 425 c.Assert(err, IsNil) 426 c.Assert(rw, NotNil) 427 428 err = rw.Write(outDir, nil) 429 c.Assert(err, ErrorMatches, `cannot write filesystem content of source:foo/: cannot specify trailing / for a source which is not a directory`) 430 } 431 432 func (s *mountedfilesystemTestSuite) TestMountedWriterErrorMissingSource(c *C) { 433 ps := &gadget.LaidOutStructure{ 434 VolumeStructure: &gadget.VolumeStructure{ 435 Size: 2048, 436 Filesystem: "ext4", 437 Content: []gadget.VolumeContent{ 438 { 439 // single file in target directory 440 Source: "foo", 441 Target: "/foo-dir/", 442 }, 443 }, 444 }, 445 } 446 447 outDir := c.MkDir() 448 449 rw, err := gadget.NewMountedFilesystemWriter(s.dir, ps, nil) 450 c.Assert(err, IsNil) 451 c.Assert(rw, NotNil) 452 453 err = rw.Write(outDir, nil) 454 c.Assert(err, ErrorMatches, "cannot write filesystem content of source:foo: .*unable to open.*: no such file or directory") 455 } 456 457 func (s *mountedfilesystemTestSuite) TestMountedWriterErrorBadDestination(c *C) { 458 makeSizedFile(c, filepath.Join(s.dir, "foo"), 0, []byte("foo foo foo")) 459 460 ps := &gadget.LaidOutStructure{ 461 VolumeStructure: &gadget.VolumeStructure{ 462 Size: 2048, 463 Filesystem: "vfat", 464 Content: []gadget.VolumeContent{ 465 { 466 // single file in target directory 467 Source: "foo", 468 Target: "/foo-dir/", 469 }, 470 }, 471 }, 472 } 473 474 outDir := c.MkDir() 475 476 err := os.Chmod(outDir, 0000) 477 c.Assert(err, IsNil) 478 479 rw, err := gadget.NewMountedFilesystemWriter(s.dir, ps, nil) 480 c.Assert(err, IsNil) 481 c.Assert(rw, NotNil) 482 483 err = rw.Write(outDir, nil) 484 c.Assert(err, ErrorMatches, "cannot write filesystem content of source:foo: cannot create .*: mkdir .* permission denied") 485 } 486 487 func (s *mountedfilesystemTestSuite) TestMountedWriterConflictingDestinationDirectoryErrors(c *C) { 488 makeGadgetData(c, s.dir, []gadgetData{ 489 {name: "foo", content: "foo foo foo"}, 490 {name: "foo-dir", content: "bar bar bar"}, 491 }) 492 493 psOverwritesDirectoryWithFile := &gadget.LaidOutStructure{ 494 VolumeStructure: &gadget.VolumeStructure{ 495 Size: 2048, 496 Filesystem: "ext4", 497 Content: []gadget.VolumeContent{ 498 { 499 // single file in target directory 500 Source: "foo", 501 Target: "/foo-dir/", 502 }, { 503 // conflicts with /foo-dir directory 504 Source: "foo-dir", 505 Target: "/", 506 }, 507 }, 508 }, 509 } 510 511 outDir := c.MkDir() 512 513 rw, err := gadget.NewMountedFilesystemWriter(s.dir, psOverwritesDirectoryWithFile, nil) 514 c.Assert(err, IsNil) 515 c.Assert(rw, NotNil) 516 517 // can't overwrite a directory with a file 518 err = rw.Write(outDir, nil) 519 c.Assert(err, ErrorMatches, fmt.Sprintf(`cannot write filesystem content of source:foo-dir: cannot copy .*: cannot commit atomic file copy: rename %[1]s/foo-dir\.[a-zA-Z0-9]+~ %[1]s/foo-dir: file exists`, outDir)) 520 521 } 522 523 func (s *mountedfilesystemTestSuite) TestMountedWriterConflictingDestinationFileOk(c *C) { 524 makeGadgetData(c, s.dir, []gadgetData{ 525 {name: "foo", content: "foo foo foo"}, 526 {name: "bar", content: "bar bar bar"}, 527 }) 528 psOverwritesFile := &gadget.LaidOutStructure{ 529 VolumeStructure: &gadget.VolumeStructure{ 530 Size: 2048, 531 Filesystem: "ext4", 532 Content: []gadget.VolumeContent{ 533 { 534 Source: "bar", 535 Target: "/", 536 }, { 537 // overwrites data from preceding entry 538 Source: "foo", 539 Target: "/bar", 540 }, 541 }, 542 }, 543 } 544 545 outDir := c.MkDir() 546 547 rw, err := gadget.NewMountedFilesystemWriter(s.dir, psOverwritesFile, nil) 548 c.Assert(err, IsNil) 549 c.Assert(rw, NotNil) 550 551 err = rw.Write(outDir, nil) 552 c.Assert(err, IsNil) 553 554 c.Check(osutil.FileExists(filepath.Join(outDir, "foo")), Equals, false) 555 // overwritten 556 c.Check(filepath.Join(outDir, "bar"), testutil.FileEquals, "foo foo foo") 557 } 558 559 func (s *mountedfilesystemTestSuite) TestMountedWriterErrorNested(c *C) { 560 makeGadgetData(c, s.dir, []gadgetData{ 561 {name: "foo/foo-dir", content: "data"}, 562 {name: "foo/bar/baz", content: "data"}, 563 }) 564 565 ps := &gadget.LaidOutStructure{ 566 VolumeStructure: &gadget.VolumeStructure{ 567 Size: 2048, 568 Filesystem: "ext4", 569 Content: []gadget.VolumeContent{ 570 { 571 // single file in target directory 572 Source: "/", 573 Target: "/foo-dir/", 574 }, 575 }, 576 }, 577 } 578 579 outDir := c.MkDir() 580 581 makeSizedFile(c, filepath.Join(outDir, "/foo-dir/foo/bar"), 0, nil) 582 583 rw, err := gadget.NewMountedFilesystemWriter(s.dir, ps, nil) 584 c.Assert(err, IsNil) 585 c.Assert(rw, NotNil) 586 587 err = rw.Write(outDir, nil) 588 c.Assert(err, ErrorMatches, "cannot write filesystem content of source:/: .* not a directory") 589 } 590 591 func (s *mountedfilesystemTestSuite) TestMountedWriterPreserve(c *C) { 592 // some data for the gadget 593 gdWritten := []gadgetData{ 594 {name: "foo", target: "foo-dir/foo", content: "data"}, 595 {name: "bar", target: "bar-name", content: "data"}, 596 {name: "boot-assets/splash", target: "splash", content: "data"}, 597 {name: "boot-assets/some-dir/data", target: "some-dir/data", content: "data"}, 598 {name: "boot-assets/some-dir/empty-file", target: "some-dir/empty-file", content: "data"}, 599 {name: "boot-assets/nested-dir/more-nested/more", target: "/nested-copy/more-nested/more", content: "data"}, 600 } 601 gdNotWritten := []gadgetData{ 602 {name: "foo", target: "/foo", content: "data"}, 603 {name: "boot-assets/some-dir/data", target: "data-copy", content: "data"}, 604 {name: "boot-assets/nested-dir/nested", target: "/nested-copy/nested", content: "data"}, 605 } 606 makeGadgetData(c, s.dir, append(gdWritten, gdNotWritten...)) 607 608 // these exist in the root directory and are preserved 609 preserve := []string{ 610 // mix entries with leading / and without 611 "/foo", 612 "/data-copy", 613 "nested-copy/nested", 614 "not-listed", // not present in 'gadget' contents 615 } 616 // these are preserved, but don't exist in the root, so data from gadget 617 // will be written 618 preserveButNotPresent := []string{ 619 "/bar-name", 620 "some-dir/data", 621 } 622 outDir := filepath.Join(c.MkDir(), "out-dir") 623 624 for _, en := range preserve { 625 p := filepath.Join(outDir, en) 626 makeSizedFile(c, p, 0, []byte("can't touch this")) 627 } 628 629 ps := &gadget.LaidOutStructure{ 630 VolumeStructure: &gadget.VolumeStructure{ 631 Size: 2048, 632 Filesystem: "ext4", 633 Content: []gadget.VolumeContent{ 634 { 635 Source: "foo", 636 Target: "/foo-dir/", 637 }, { 638 // would overwrite /foo 639 Source: "foo", 640 Target: "/", 641 }, { 642 // preserved, but not present, will be 643 // written 644 Source: "bar", 645 Target: "/bar-name", 646 }, { 647 // some-dir/data is preserved, but not 648 // preset, hence will be written 649 Source: "boot-assets/", 650 Target: "/", 651 }, { 652 // would overwrite /data-copy 653 Source: "boot-assets/some-dir/data", 654 Target: "/data-copy", 655 }, { 656 // would overwrite /nested-copy/nested 657 Source: "boot-assets/nested-dir/", 658 Target: "/nested-copy/", 659 }, 660 }, 661 }, 662 } 663 664 rw, err := gadget.NewMountedFilesystemWriter(s.dir, ps, nil) 665 c.Assert(err, IsNil) 666 c.Assert(rw, NotNil) 667 668 err = rw.Write(outDir, append(preserve, preserveButNotPresent...)) 669 c.Assert(err, IsNil) 670 671 // files that existed were preserved 672 for _, en := range preserve { 673 p := filepath.Join(outDir, en) 674 c.Check(p, testutil.FileEquals, "can't touch this") 675 } 676 // everything else was written 677 verifyWrittenGadgetData(c, outDir, gdWritten) 678 } 679 680 func (s *mountedfilesystemTestSuite) TestMountedWriterPreserveWithObserver(c *C) { 681 // some data for the gadget 682 gd := []gadgetData{ 683 {name: "foo", target: "foo-dir/foo", content: "foo from gadget"}, 684 } 685 makeGadgetData(c, s.dir, gd) 686 687 outDir := filepath.Join(c.MkDir(), "out-dir") 688 makeSizedFile(c, filepath.Join(outDir, "foo"), 0, []byte("foo from disk")) 689 690 ps := &gadget.LaidOutStructure{ 691 VolumeStructure: &gadget.VolumeStructure{ 692 Size: 2048, 693 Filesystem: "ext4", 694 Content: []gadget.VolumeContent{ 695 { 696 Source: "foo", 697 // would overwrite existing foo 698 Target: "foo", 699 }, { 700 Source: "foo", 701 // does not exist 702 Target: "foo-new", 703 }, 704 }, 705 }, 706 } 707 708 obs := &mockWriteObserver{ 709 c: c, 710 expectedStruct: ps, 711 preserveTargets: []string{ 712 "foo", 713 "foo-new", 714 }, 715 } 716 rw, err := gadget.NewMountedFilesystemWriter(s.dir, ps, obs) 717 c.Assert(err, IsNil) 718 c.Assert(rw, NotNil) 719 720 err = rw.Write(outDir, nil) 721 c.Assert(err, IsNil) 722 c.Check(filepath.Join(outDir, "foo"), testutil.FileEquals, "foo from disk") 723 c.Check(filepath.Join(outDir, "foo-new"), testutil.FileAbsent) 724 } 725 726 func (s *mountedfilesystemTestSuite) TestMountedWriterNonFilePreserveError(c *C) { 727 // some data for the gadget 728 gd := []gadgetData{ 729 {name: "foo", content: "data"}, 730 } 731 makeGadgetData(c, s.dir, gd) 732 733 preserve := []string{ 734 // this will be a directory 735 "foo", 736 } 737 outDir := filepath.Join(c.MkDir(), "out-dir") 738 // will conflict with preserve entry 739 err := os.MkdirAll(filepath.Join(outDir, "foo"), 0755) 740 c.Assert(err, IsNil) 741 742 ps := &gadget.LaidOutStructure{ 743 VolumeStructure: &gadget.VolumeStructure{ 744 Size: 2048, 745 Filesystem: "ext4", 746 Content: []gadget.VolumeContent{ 747 { 748 Source: "/", 749 Target: "/", 750 }, 751 }, 752 }, 753 } 754 755 rw, err := gadget.NewMountedFilesystemWriter(s.dir, ps, nil) 756 c.Assert(err, IsNil) 757 c.Assert(rw, NotNil) 758 759 err = rw.Write(outDir, preserve) 760 c.Assert(err, ErrorMatches, `cannot map preserve entries for destination ".*/out-dir": preserved entry "foo" cannot be a directory`) 761 } 762 763 func (s *mountedfilesystemTestSuite) TestMountedWriterImplicitDir(c *C) { 764 gd := []gadgetData{ 765 {name: "boot-assets/nested-dir/nested", target: "/nested-copy/nested-dir/nested", content: "nested"}, 766 {name: "boot-assets/nested-dir/more-nested/more", target: "/nested-copy/nested-dir/more-nested/more", content: "more"}, 767 } 768 makeGadgetData(c, s.dir, gd) 769 770 ps := &gadget.LaidOutStructure{ 771 VolumeStructure: &gadget.VolumeStructure{ 772 Size: 2048, 773 Filesystem: "ext4", 774 Content: []gadget.VolumeContent{ 775 { 776 // contents of nested directory under new target directory 777 Source: "boot-assets/nested-dir", 778 Target: "/nested-copy/", 779 }, 780 }, 781 }, 782 } 783 784 outDir := c.MkDir() 785 786 rw, err := gadget.NewMountedFilesystemWriter(s.dir, ps, nil) 787 c.Assert(err, IsNil) 788 c.Assert(rw, NotNil) 789 790 err = rw.Write(outDir, nil) 791 c.Assert(err, IsNil) 792 793 verifyWrittenGadgetData(c, outDir, gd) 794 } 795 796 func (s *mountedfilesystemTestSuite) TestMountedWriterNoFs(c *C) { 797 ps := &gadget.LaidOutStructure{ 798 VolumeStructure: &gadget.VolumeStructure{ 799 Size: 2048, 800 // no filesystem 801 Content: []gadget.VolumeContent{ 802 { 803 // single file in target directory 804 Source: "foo", 805 Target: "/foo-dir/", 806 }, 807 }, 808 }, 809 } 810 811 rw, err := gadget.NewMountedFilesystemWriter(s.dir, ps, nil) 812 c.Assert(err, ErrorMatches, "structure #0 has no filesystem") 813 c.Assert(rw, IsNil) 814 } 815 816 func (s *mountedfilesystemTestSuite) TestMountedWriterTrivialValidation(c *C) { 817 rw, err := gadget.NewMountedFilesystemWriter(s.dir, nil, nil) 818 c.Assert(err, ErrorMatches, `internal error: \*LaidOutStructure.*`) 819 c.Assert(rw, IsNil) 820 821 ps := &gadget.LaidOutStructure{ 822 VolumeStructure: &gadget.VolumeStructure{ 823 Size: 2048, 824 Filesystem: "ext4", 825 // no filesystem 826 Content: []gadget.VolumeContent{ 827 { 828 Source: "", 829 Target: "", 830 }, 831 }, 832 }, 833 } 834 835 rw, err = gadget.NewMountedFilesystemWriter("", ps, nil) 836 c.Assert(err, ErrorMatches, `internal error: gadget content directory cannot be unset`) 837 c.Assert(rw, IsNil) 838 839 rw, err = gadget.NewMountedFilesystemWriter(s.dir, ps, nil) 840 c.Assert(err, IsNil) 841 842 err = rw.Write("", nil) 843 c.Assert(err, ErrorMatches, "internal error: destination directory cannot be unset") 844 845 d := c.MkDir() 846 err = rw.Write(d, nil) 847 c.Assert(err, ErrorMatches, "cannot write filesystem content .* source cannot be unset") 848 849 ps.Content[0].Source = "/" 850 err = rw.Write(d, nil) 851 c.Assert(err, ErrorMatches, "cannot write filesystem content .* target cannot be unset") 852 } 853 854 func (s *mountedfilesystemTestSuite) TestMountedWriterSymlinks(c *C) { 855 // some data for the gadget 856 gd := []gadgetData{ 857 {name: "foo", target: "foo", content: "data"}, 858 {name: "nested/foo", target: "nested/foo", content: "nested-data"}, 859 {name: "link", symlinkTo: "foo"}, 860 {name: "nested-link", symlinkTo: "nested"}, 861 } 862 makeGadgetData(c, s.dir, gd) 863 864 outDir := filepath.Join(c.MkDir(), "out-dir") 865 866 ps := &gadget.LaidOutStructure{ 867 VolumeStructure: &gadget.VolumeStructure{ 868 Size: 2048, 869 Filesystem: "ext4", 870 Content: []gadget.VolumeContent{ 871 {Source: "/", Target: "/"}, 872 }, 873 }, 874 } 875 876 rw, err := gadget.NewMountedFilesystemWriter(s.dir, ps, nil) 877 c.Assert(err, IsNil) 878 c.Assert(rw, NotNil) 879 880 err = rw.Write(outDir, nil) 881 c.Assert(err, IsNil) 882 883 // everything else was written 884 verifyWrittenGadgetData(c, outDir, []gadgetData{ 885 {target: "foo", content: "data"}, 886 {target: "link", symlinkTo: "foo"}, 887 {target: "nested/foo", content: "nested-data"}, 888 {target: "nested-link", symlinkTo: "nested"}, 889 // when read via symlink 890 {target: "nested-link/foo", content: "nested-data"}, 891 }) 892 } 893 894 type mockContentUpdateObserver struct { 895 contentUpdate map[string][]*mockContentChange 896 contentRollback map[string][]*mockContentChange 897 preserveTargets []string 898 observeErr error 899 expectedStruct *gadget.LaidOutStructure 900 c *C 901 } 902 903 func (m *mockContentUpdateObserver) reset() { 904 m.contentUpdate = nil 905 m.contentRollback = nil 906 } 907 908 func (m *mockContentUpdateObserver) Observe(op gadget.ContentOperation, sourceStruct *gadget.LaidOutStructure, 909 targetRootDir, relativeTargetPath string, data *gadget.ContentChange) (gadget.ContentChangeAction, error) { 910 if m.c == nil { 911 panic("c is unset") 912 } 913 if m.contentUpdate == nil { 914 m.contentUpdate = make(map[string][]*mockContentChange) 915 } 916 if m.contentRollback == nil { 917 m.contentRollback = make(map[string][]*mockContentChange) 918 } 919 m.c.Assert(data, NotNil) 920 921 // the after content must always be set 922 m.c.Check(osutil.FileExists(data.After) && !osutil.IsDirectory(data.After), Equals, true, 923 Commentf("after reference path %q does not exist or is a directory", data.After)) 924 // they may be no before content for new files 925 if data.Before != "" { 926 m.c.Check(osutil.FileExists(data.Before) && !osutil.IsDirectory(data.Before), Equals, true, 927 Commentf("before reference path %q does not exist or is a directory", data.Before)) 928 } 929 m.c.Check(filepath.IsAbs(relativeTargetPath), Equals, false, 930 Commentf("target path %q is absolute", relativeTargetPath)) 931 932 opData := &mockContentChange{path: relativeTargetPath, change: data} 933 switch op { 934 case gadget.ContentUpdate: 935 m.contentUpdate[targetRootDir] = append(m.contentUpdate[targetRootDir], opData) 936 case gadget.ContentRollback: 937 m.contentRollback[targetRootDir] = append(m.contentRollback[targetRootDir], opData) 938 default: 939 m.c.Fatalf("unexpected observe operation %v", op) 940 } 941 942 m.c.Assert(sourceStruct, NotNil) 943 m.c.Check(m.expectedStruct, DeepEquals, sourceStruct) 944 945 if m.observeErr != nil { 946 return gadget.ChangeAbort, m.observeErr 947 } 948 if strutil.ListContains(m.preserveTargets, relativeTargetPath) { 949 return gadget.ChangeIgnore, nil 950 } 951 return gadget.ChangeApply, nil 952 } 953 954 func (s *mountedfilesystemTestSuite) TestMountedUpdaterBackupSimple(c *C) { 955 // some data for the gadget 956 gdWritten := []gadgetData{ 957 {name: "bar", target: "bar-name", content: "data"}, 958 {name: "foo", target: "foo", content: "data"}, 959 {name: "zed", target: "zed", content: "data"}, 960 {name: "same-data", target: "same", content: "same"}, 961 // not included in volume contents 962 {name: "not-written", target: "not-written", content: "data"}, 963 } 964 makeGadgetData(c, s.dir, gdWritten) 965 966 outDir := filepath.Join(c.MkDir(), "out-dir") 967 968 // these exist in the destination directory and will be backed up 969 backedUp := []gadgetData{ 970 {target: "foo", content: "can't touch this"}, 971 {target: "nested/foo", content: "can't touch this"}, 972 // listed in preserve 973 {target: "zed", content: "preserved"}, 974 // same content as the update 975 {target: "same", content: "same"}, 976 } 977 makeExistingData(c, outDir, backedUp) 978 979 ps := &gadget.LaidOutStructure{ 980 VolumeStructure: &gadget.VolumeStructure{ 981 Size: 2048, 982 Filesystem: "ext4", 983 Content: []gadget.VolumeContent{ 984 { 985 Source: "bar", 986 Target: "/bar-name", 987 }, { 988 Source: "foo", 989 Target: "/", 990 }, { 991 Source: "foo", 992 Target: "/nested/", 993 }, { 994 Source: "zed", 995 Target: "/", 996 }, { 997 Source: "same-data", 998 Target: "/same", 999 }, 1000 }, 1001 Update: gadget.VolumeUpdate{ 1002 Edition: 1, 1003 Preserve: []string{"/zed"}, 1004 }, 1005 }, 1006 } 1007 1008 muo := &mockContentUpdateObserver{ 1009 c: c, 1010 expectedStruct: ps, 1011 } 1012 rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) { 1013 c.Check(to, DeepEquals, ps) 1014 return outDir, nil 1015 }, muo) 1016 c.Assert(err, IsNil) 1017 c.Assert(rw, NotNil) 1018 1019 err = rw.Backup() 1020 c.Assert(err, IsNil) 1021 1022 // files that existed were backed up 1023 for _, en := range backedUp { 1024 backup := filepath.Join(s.backup, "struct-0", en.target+".backup") 1025 same := filepath.Join(s.backup, "struct-0", en.target+".same") 1026 switch en.content { 1027 case "preserved": 1028 c.Check(osutil.FileExists(backup), Equals, false, Commentf("file: %v", backup)) 1029 c.Check(osutil.FileExists(same), Equals, false, Commentf("file: %v", same)) 1030 case "same": 1031 c.Check(osutil.FileExists(same), Equals, true, Commentf("file: %v", same)) 1032 default: 1033 c.Check(backup, testutil.FileEquals, "can't touch this") 1034 } 1035 } 1036 // notified for both updated and new content 1037 c.Check(muo.contentUpdate, DeepEquals, map[string][]*mockContentChange{ 1038 outDir: { 1039 // bar-name is a new file 1040 {"bar-name", &gadget.ContentChange{ 1041 After: filepath.Join(s.dir, "bar"), 1042 }}, 1043 // updates 1044 {"foo", &gadget.ContentChange{ 1045 After: filepath.Join(s.dir, "foo"), 1046 Before: filepath.Join(s.backup, "struct-0/foo.backup"), 1047 }}, 1048 {"nested/foo", &gadget.ContentChange{ 1049 After: filepath.Join(s.dir, "foo"), 1050 Before: filepath.Join(s.backup, "struct-0/nested/foo.backup"), 1051 }}, 1052 }, 1053 }) 1054 1055 // running backup again (eg. after a reboot) does not error out 1056 err = rw.Backup() 1057 c.Assert(err, IsNil) 1058 // we are notified of all files again 1059 c.Check(muo.contentUpdate, DeepEquals, map[string][]*mockContentChange{ 1060 outDir: { 1061 // bar-name is a new file 1062 {"bar-name", &gadget.ContentChange{ 1063 After: filepath.Join(s.dir, "bar"), 1064 }}, 1065 {"foo", &gadget.ContentChange{ 1066 After: filepath.Join(s.dir, "foo"), 1067 Before: filepath.Join(s.backup, "struct-0/foo.backup"), 1068 }}, 1069 {"nested/foo", &gadget.ContentChange{ 1070 After: filepath.Join(s.dir, "foo"), 1071 Before: filepath.Join(s.backup, "struct-0/nested/foo.backup"), 1072 }}, 1073 // same set of calls once more 1074 {"bar-name", &gadget.ContentChange{ 1075 After: filepath.Join(s.dir, "bar"), 1076 }}, 1077 {"foo", &gadget.ContentChange{ 1078 After: filepath.Join(s.dir, "foo"), 1079 Before: filepath.Join(s.backup, "struct-0/foo.backup"), 1080 }}, 1081 {"nested/foo", &gadget.ContentChange{ 1082 After: filepath.Join(s.dir, "foo"), 1083 Before: filepath.Join(s.backup, "struct-0/nested/foo.backup"), 1084 }}, 1085 }, 1086 }) 1087 } 1088 1089 func (s *mountedfilesystemTestSuite) TestMountedWriterObserverErr(c *C) { 1090 makeGadgetData(c, s.dir, []gadgetData{ 1091 {name: "foo", content: "data"}, 1092 }) 1093 1094 ps := &gadget.LaidOutStructure{ 1095 VolumeStructure: &gadget.VolumeStructure{ 1096 Size: 2048, 1097 Filesystem: "ext4", 1098 Content: []gadget.VolumeContent{ 1099 {Source: "/", Target: "/"}, 1100 }, 1101 }, 1102 } 1103 1104 outDir := c.MkDir() 1105 obs := &mockWriteObserver{ 1106 c: c, 1107 observeErr: errors.New("observe fail"), 1108 expectedStruct: ps, 1109 } 1110 rw, err := gadget.NewMountedFilesystemWriter(s.dir, ps, obs) 1111 c.Assert(err, IsNil) 1112 c.Assert(rw, NotNil) 1113 1114 err = rw.Write(outDir, nil) 1115 c.Assert(err, ErrorMatches, "cannot write filesystem content of source:/: cannot observe file write: observe fail") 1116 } 1117 1118 func (s *mountedfilesystemTestSuite) TestMountedUpdaterBackupWithDirectories(c *C) { 1119 // some data for the gadget 1120 gdWritten := []gadgetData{ 1121 {name: "bar", content: "data"}, 1122 {name: "some-dir/foo", content: "data"}, 1123 {name: "empty-dir/"}, 1124 } 1125 makeGadgetData(c, s.dir, gdWritten) 1126 1127 outDir := filepath.Join(c.MkDir(), "out-dir") 1128 1129 // these exist in the destination directory and will be backed up 1130 backedUp := []gadgetData{ 1131 // overwritten by "bar" -> "/foo" 1132 {target: "foo", content: "can't touch this"}, 1133 // overwritten by some-dir/ -> /nested/ 1134 {target: "nested/foo", content: "can't touch this"}, 1135 // written to by bar -> /this/is/some/nested/ 1136 {target: "this/is/some/"}, 1137 {target: "lone-dir/"}, 1138 } 1139 makeExistingData(c, outDir, backedUp) 1140 1141 ps := &gadget.LaidOutStructure{ 1142 VolumeStructure: &gadget.VolumeStructure{ 1143 Size: 2048, 1144 Filesystem: "ext4", 1145 Content: []gadget.VolumeContent{ 1146 { 1147 Source: "bar", 1148 Target: "/foo", 1149 }, { 1150 Source: "bar", 1151 Target: "/this/is/some/nested/", 1152 }, { 1153 Source: "some-dir/", 1154 Target: "/nested/", 1155 }, { 1156 Source: "empty-dir/", 1157 Target: "/lone-dir/", 1158 }, 1159 }, 1160 Update: gadget.VolumeUpdate{ 1161 Edition: 1, 1162 Preserve: []string{"/zed"}, 1163 }, 1164 }, 1165 } 1166 1167 rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) { 1168 c.Check(to, DeepEquals, ps) 1169 return outDir, nil 1170 }, nil) 1171 c.Assert(err, IsNil) 1172 c.Assert(rw, NotNil) 1173 1174 err = rw.Backup() 1175 c.Assert(err, IsNil) 1176 1177 verifyDirContents(c, filepath.Join(s.backup, "struct-0"), map[string]contentType{ 1178 "this/is/some.backup": typeFile, 1179 "this/is.backup": typeFile, 1180 "this.backup": typeFile, 1181 1182 "nested/foo.backup": typeFile, 1183 "nested.backup": typeFile, 1184 1185 "foo.backup": typeFile, 1186 1187 "lone-dir.backup": typeFile, 1188 }) 1189 } 1190 1191 func (s *mountedfilesystemTestSuite) TestMountedUpdaterBackupNonexistent(c *C) { 1192 // some data for the gadget 1193 gd := []gadgetData{ 1194 {name: "bar", target: "foo", content: "data"}, 1195 {name: "bar", target: "some-dir/foo", content: "data"}, 1196 } 1197 makeGadgetData(c, s.dir, gd) 1198 1199 outDir := filepath.Join(c.MkDir(), "out-dir") 1200 1201 ps := &gadget.LaidOutStructure{ 1202 VolumeStructure: &gadget.VolumeStructure{ 1203 Size: 2048, 1204 Filesystem: "ext4", 1205 Content: []gadget.VolumeContent{ 1206 { 1207 Source: "bar", 1208 Target: "/foo", 1209 }, { 1210 Source: "bar", 1211 Target: "/some-dir/foo", 1212 }, 1213 }, 1214 Update: gadget.VolumeUpdate{ 1215 Edition: 1, 1216 // bar not in preserved files 1217 }, 1218 }, 1219 } 1220 1221 rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) { 1222 c.Check(to, DeepEquals, ps) 1223 return outDir, nil 1224 }, nil) 1225 c.Assert(err, IsNil) 1226 c.Assert(rw, NotNil) 1227 1228 err = rw.Backup() 1229 c.Assert(err, IsNil) 1230 1231 backupRoot := filepath.Join(s.backup, "struct-0") 1232 // actually empty 1233 verifyDirContents(c, backupRoot, map[string]contentType{}) 1234 } 1235 1236 func (s *mountedfilesystemTestSuite) TestMountedUpdaterBackupFailsOnBackupDirErrors(c *C) { 1237 outDir := filepath.Join(c.MkDir(), "out-dir") 1238 1239 ps := &gadget.LaidOutStructure{ 1240 VolumeStructure: &gadget.VolumeStructure{ 1241 Size: 2048, 1242 Filesystem: "ext4", 1243 Content: []gadget.VolumeContent{ 1244 { 1245 Source: "bar", 1246 Target: "/foo", 1247 }, 1248 }, 1249 Update: gadget.VolumeUpdate{ 1250 Edition: 1, 1251 }, 1252 }, 1253 } 1254 1255 rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) { 1256 c.Check(to, DeepEquals, ps) 1257 return outDir, nil 1258 }, nil) 1259 c.Assert(err, IsNil) 1260 c.Assert(rw, NotNil) 1261 1262 err = os.Chmod(s.backup, 0555) 1263 c.Assert(err, IsNil) 1264 defer os.Chmod(s.backup, 0755) 1265 1266 err = rw.Backup() 1267 c.Assert(err, ErrorMatches, "cannot create backup directory: .*/struct-0: permission denied") 1268 } 1269 1270 func (s *mountedfilesystemTestSuite) TestMountedUpdaterBackupFailsOnDestinationErrors(c *C) { 1271 // some data for the gadget 1272 gd := []gadgetData{ 1273 {name: "bar", content: "data"}, 1274 } 1275 makeGadgetData(c, s.dir, gd) 1276 1277 outDir := filepath.Join(c.MkDir(), "out-dir") 1278 makeExistingData(c, outDir, []gadgetData{ 1279 {target: "foo", content: "same"}, 1280 }) 1281 1282 err := os.Chmod(filepath.Join(outDir, "foo"), 0000) 1283 c.Assert(err, IsNil) 1284 1285 ps := &gadget.LaidOutStructure{ 1286 VolumeStructure: &gadget.VolumeStructure{ 1287 Size: 2048, 1288 Filesystem: "ext4", 1289 Content: []gadget.VolumeContent{ 1290 { 1291 Source: "bar", 1292 Target: "/foo", 1293 }, 1294 }, 1295 Update: gadget.VolumeUpdate{ 1296 Edition: 1, 1297 }, 1298 }, 1299 } 1300 1301 rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) { 1302 c.Check(to, DeepEquals, ps) 1303 return outDir, nil 1304 }, nil) 1305 c.Assert(err, IsNil) 1306 c.Assert(rw, NotNil) 1307 1308 err = rw.Backup() 1309 c.Assert(err, ErrorMatches, "cannot backup content: cannot open destination file: open .*/out-dir/foo: permission denied") 1310 } 1311 1312 func (s *mountedfilesystemTestSuite) TestMountedUpdaterBackupFailsOnBadSrcComparison(c *C) { 1313 // some data for the gadget 1314 gd := []gadgetData{ 1315 {name: "bar", content: "data"}, 1316 } 1317 makeGadgetData(c, s.dir, gd) 1318 err := os.Chmod(filepath.Join(s.dir, "bar"), 0000) 1319 c.Assert(err, IsNil) 1320 1321 outDir := filepath.Join(c.MkDir(), "out-dir") 1322 makeExistingData(c, outDir, []gadgetData{ 1323 {target: "foo", content: "same"}, 1324 }) 1325 1326 ps := &gadget.LaidOutStructure{ 1327 VolumeStructure: &gadget.VolumeStructure{ 1328 Size: 2048, 1329 Filesystem: "ext4", 1330 Content: []gadget.VolumeContent{ 1331 { 1332 Source: "bar", 1333 Target: "/foo", 1334 }, 1335 }, 1336 Update: gadget.VolumeUpdate{ 1337 Edition: 1, 1338 }, 1339 }, 1340 } 1341 1342 rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) { 1343 c.Check(to, DeepEquals, ps) 1344 return outDir, nil 1345 }, nil) 1346 c.Assert(err, IsNil) 1347 c.Assert(rw, NotNil) 1348 1349 err = rw.Backup() 1350 c.Assert(err, ErrorMatches, "cannot backup content: cannot checksum update file: open .*/bar: permission denied") 1351 } 1352 1353 func (s *mountedfilesystemTestSuite) TestMountedUpdaterBackupFunnyNamesConflictBackup(c *C) { 1354 gdWritten := []gadgetData{ 1355 {name: "bar.backup/foo", content: "data"}, 1356 {name: "bar", content: "data"}, 1357 {name: "foo.same/foo", content: "same-as-current"}, 1358 {name: "foo", content: "same-as-current"}, 1359 } 1360 makeGadgetData(c, s.dir, gdWritten) 1361 1362 // backup stamps conflicts with bar.backup 1363 existingUpBar := []gadgetData{ 1364 // will be listed first 1365 {target: "bar", content: "can't touch this"}, 1366 {target: "bar.backup/foo", content: "can't touch this"}, 1367 } 1368 // backup stamps conflicts with foo.same 1369 existingUpFoo := []gadgetData{ 1370 // will be listed first 1371 {target: "foo", content: "same-as-current"}, 1372 {target: "foo.same/foo", content: "can't touch this"}, 1373 } 1374 1375 outDirConflictsBar := filepath.Join(c.MkDir(), "out-dir-bar") 1376 makeExistingData(c, outDirConflictsBar, existingUpBar) 1377 1378 outDirConflictsFoo := filepath.Join(c.MkDir(), "out-dir-foo") 1379 makeExistingData(c, outDirConflictsFoo, existingUpFoo) 1380 1381 ps := &gadget.LaidOutStructure{ 1382 VolumeStructure: &gadget.VolumeStructure{ 1383 Size: 2048, 1384 Filesystem: "ext4", 1385 Content: []gadget.VolumeContent{ 1386 {Source: "/", Target: "/"}, 1387 }, 1388 Update: gadget.VolumeUpdate{ 1389 Edition: 1, 1390 }, 1391 }, 1392 } 1393 1394 backupBar := filepath.Join(s.backup, "backup-bar") 1395 backupFoo := filepath.Join(s.backup, "backup-foo") 1396 1397 prefix := `cannot backup content: cannot create backup file: cannot create stamp file prefix: ` 1398 for _, tc := range []struct { 1399 backupDir string 1400 outDir string 1401 err string 1402 }{ 1403 {backupBar, outDirConflictsBar, prefix + `mkdir .*/bar.backup: not a directory`}, 1404 {backupFoo, outDirConflictsFoo, prefix + `mkdir .*/foo.same: not a directory`}, 1405 } { 1406 err := os.MkdirAll(tc.backupDir, 0755) 1407 c.Assert(err, IsNil) 1408 rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, tc.backupDir, func(to *gadget.LaidOutStructure) (string, error) { 1409 c.Check(to, DeepEquals, ps) 1410 return tc.outDir, nil 1411 }, nil) 1412 c.Assert(err, IsNil) 1413 c.Assert(rw, NotNil) 1414 1415 err = rw.Backup() 1416 c.Assert(err, ErrorMatches, tc.err) 1417 } 1418 } 1419 1420 func (s *mountedfilesystemTestSuite) TestMountedUpdaterBackupFunnyNamesOk(c *C) { 1421 gdWritten := []gadgetData{ 1422 {name: "bar.backup/foo", target: "bar.backup/foo", content: "data"}, 1423 {name: "foo.same/foo.same", target: "foo.same/foo.same", content: "same-as-current"}, 1424 {name: "zed.preserve", target: "zed.preserve", content: "this-is-preserved"}, 1425 {name: "new-file.same", target: "new-file.same", content: "this-is-new"}, 1426 } 1427 makeGadgetData(c, s.dir, gdWritten) 1428 1429 outDir := filepath.Join(c.MkDir(), "out-dir") 1430 1431 // these exist in the destination directory and will be backed up 1432 backedUp := []gadgetData{ 1433 // will be listed first 1434 {target: "bar.backup/foo", content: "not-data"}, 1435 {target: "foo.same/foo.same", content: "same-as-current"}, 1436 {target: "zed.preserve", content: "to-be-preserved"}, 1437 } 1438 makeExistingData(c, outDir, backedUp) 1439 1440 ps := &gadget.LaidOutStructure{ 1441 VolumeStructure: &gadget.VolumeStructure{ 1442 Size: 2048, 1443 Filesystem: "ext4", 1444 Content: []gadget.VolumeContent{ 1445 {Source: "/", Target: "/"}, 1446 }, 1447 Update: gadget.VolumeUpdate{ 1448 Edition: 1, 1449 Preserve: []string{ 1450 "zed.preserve", 1451 }, 1452 }, 1453 }, 1454 } 1455 1456 rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) { 1457 c.Check(to, DeepEquals, ps) 1458 return outDir, nil 1459 }, nil) 1460 c.Assert(err, IsNil) 1461 c.Assert(rw, NotNil) 1462 1463 err = rw.Backup() 1464 c.Assert(err, IsNil) 1465 1466 verifyDirContents(c, filepath.Join(s.backup, "struct-0"), map[string]contentType{ 1467 "bar.backup.backup": typeFile, 1468 "bar.backup/foo.backup": typeFile, 1469 1470 "foo.same.backup": typeFile, 1471 "foo.same/foo.same.same": typeFile, 1472 1473 "zed.preserve.preserve": typeFile, 1474 }) 1475 } 1476 1477 func (s *mountedfilesystemTestSuite) TestMountedUpdaterBackupErrorOnSymlinkFile(c *C) { 1478 gd := []gadgetData{ 1479 {name: "bar/data", target: "bar/data", content: "some data"}, 1480 {name: "bar/foo", target: "bar/foo", content: "data"}, 1481 } 1482 makeGadgetData(c, s.dir, gd) 1483 1484 outDir := filepath.Join(c.MkDir(), "out-dir") 1485 1486 existing := []gadgetData{ 1487 {target: "bar/data", content: "some data"}, 1488 {target: "bar/foo", symlinkTo: "data"}, 1489 } 1490 makeExistingData(c, outDir, existing) 1491 1492 ps := &gadget.LaidOutStructure{ 1493 VolumeStructure: &gadget.VolumeStructure{ 1494 Size: 2048, 1495 Filesystem: "ext4", 1496 Content: []gadget.VolumeContent{ 1497 {Source: "/", Target: "/"}, 1498 }, 1499 }, 1500 } 1501 1502 rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) { 1503 c.Check(to, DeepEquals, ps) 1504 return outDir, nil 1505 }, nil) 1506 c.Assert(err, IsNil) 1507 c.Assert(rw, NotNil) 1508 1509 err = rw.Backup() 1510 c.Assert(err, ErrorMatches, "cannot backup content: cannot backup file /bar/foo: symbolic links are not supported") 1511 } 1512 1513 func (s *mountedfilesystemTestSuite) TestMountedUpdaterBackupErrorOnSymlinkInPrefixDir(c *C) { 1514 gd := []gadgetData{ 1515 {name: "bar/nested/data", target: "bar/data", content: "some data"}, 1516 {name: "baz/foo", target: "baz/foo", content: "data"}, 1517 } 1518 makeGadgetData(c, s.dir, gd) 1519 1520 outDir := filepath.Join(c.MkDir(), "out-dir") 1521 1522 existing := []gadgetData{ 1523 {target: "bar/nested-target/data", content: "some data"}, 1524 } 1525 makeExistingData(c, outDir, existing) 1526 // bar/nested-target -> nested 1527 os.Symlink("nested-target", filepath.Join(outDir, "bar/nested")) 1528 1529 ps := &gadget.LaidOutStructure{ 1530 VolumeStructure: &gadget.VolumeStructure{ 1531 Size: 2048, 1532 Filesystem: "ext4", 1533 Content: []gadget.VolumeContent{ 1534 {Source: "/", Target: "/"}, 1535 }, 1536 }, 1537 } 1538 1539 rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) { 1540 c.Check(to, DeepEquals, ps) 1541 return outDir, nil 1542 }, nil) 1543 c.Assert(err, IsNil) 1544 c.Assert(rw, NotNil) 1545 1546 err = rw.Backup() 1547 c.Assert(err, ErrorMatches, "cannot backup content: cannot create a checkpoint for directory /bar/nested: symbolic links are not supported") 1548 } 1549 1550 func (s *mountedfilesystemTestSuite) TestMountedUpdaterUpdate(c *C) { 1551 // some data for the gadget 1552 gdWritten := []gadgetData{ 1553 {name: "foo", target: "foo-dir/foo", content: "data"}, 1554 {name: "bar", target: "bar-name", content: "data"}, 1555 {name: "boot-assets/splash", target: "splash", content: "data"}, 1556 {name: "boot-assets/some-dir/data", target: "some-dir/data", content: "data"}, 1557 {name: "boot-assets/some-dir/empty-file", target: "some-dir/empty-file", content: ""}, 1558 {name: "boot-assets/nested-dir/more-nested/more", target: "/nested-copy/more-nested/more", content: "data"}, 1559 } 1560 // data inside the gadget that will be skipped due to being part of 1561 // 'preserve' list 1562 gdNotWritten := []gadgetData{ 1563 {name: "foo", target: "/foo", content: "data"}, 1564 {name: "boot-assets/some-dir/data", target: "data-copy", content: "data"}, 1565 {name: "boot-assets/nested-dir/nested", target: "/nested-copy/nested", content: "data"}, 1566 } 1567 // data inside the gadget that is identical to what is already present in the target 1568 gdIdentical := []gadgetData{ 1569 {name: "boot-assets/nested-dir/more-nested/identical", target: "/nested-copy/more-nested/identical", content: "same-as-target"}, 1570 {name: "boot-assets/nested-dir/same-as-target-dir/identical", target: "/nested-copy/same-as-target-dir/identical", content: "same-as-target"}, 1571 } 1572 1573 gd := append(gdWritten, gdNotWritten...) 1574 gd = append(gd, gdIdentical...) 1575 makeGadgetData(c, s.dir, gd) 1576 1577 // these exist in the root directory and are preserved 1578 preserve := []string{ 1579 // mix entries with leading / and without 1580 "/foo", 1581 "/data-copy", 1582 "nested-copy/nested", 1583 "not-listed", // not present in 'gadget' contents 1584 } 1585 // these are preserved, but don't exist in the root, so data from gadget 1586 // will be written 1587 preserveButNotPresent := []string{ 1588 "/bar-name", 1589 "some-dir/data", 1590 } 1591 outDir := filepath.Join(c.MkDir(), "out-dir") 1592 1593 for _, en := range preserve { 1594 p := filepath.Join(outDir, en) 1595 makeSizedFile(c, p, 0, []byte("can't touch this")) 1596 } 1597 for _, en := range gdIdentical { 1598 makeSizedFile(c, filepath.Join(outDir, en.target), 0, []byte(en.content)) 1599 } 1600 1601 ps := &gadget.LaidOutStructure{ 1602 VolumeStructure: &gadget.VolumeStructure{ 1603 Size: 2048, 1604 Filesystem: "ext4", 1605 Content: []gadget.VolumeContent{ 1606 { 1607 Source: "foo", 1608 Target: "/foo-dir/", 1609 }, { 1610 // would overwrite /foo 1611 Source: "foo", 1612 Target: "/", 1613 }, { 1614 // preserved, but not present, will be 1615 // written 1616 Source: "bar", 1617 Target: "/bar-name", 1618 }, { 1619 // some-dir/data is preserved, but not 1620 // present, hence will be written 1621 Source: "boot-assets/", 1622 Target: "/", 1623 }, { 1624 // would overwrite /data-copy 1625 Source: "boot-assets/some-dir/data", 1626 Target: "/data-copy", 1627 }, { 1628 // would overwrite /nested-copy/nested 1629 Source: "boot-assets/nested-dir/", 1630 Target: "/nested-copy/", 1631 }, { 1632 Source: "boot-assets", 1633 Target: "/boot-assets-copy/", 1634 }, 1635 }, 1636 Update: gadget.VolumeUpdate{ 1637 Edition: 1, 1638 Preserve: append(preserve, preserveButNotPresent...), 1639 }, 1640 }, 1641 } 1642 1643 muo := &mockContentUpdateObserver{ 1644 c: c, 1645 expectedStruct: ps, 1646 } 1647 rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) { 1648 c.Check(to, DeepEquals, ps) 1649 return outDir, nil 1650 }, muo) 1651 c.Assert(err, IsNil) 1652 c.Assert(rw, NotNil) 1653 1654 err = rw.Backup() 1655 c.Assert(err, IsNil) 1656 1657 // identical files were identified as such 1658 for _, en := range gdIdentical { 1659 c.Check(filepath.Join(s.backup, "struct-0", en.target)+".same", testutil.FilePresent) 1660 } 1661 1662 // only notified about content getting updated 1663 c.Check(muo.contentUpdate, DeepEquals, map[string][]*mockContentChange{ 1664 outDir: { 1665 // the following files were not observed because they 1666 // are the same as the ones on disk: 1667 // - nested-copy/more-nested/identical 1668 // - nested-copy/same-as-target-dir/identical 1669 // 1670 // we still get notified about new files: 1671 {"foo-dir/foo", &gadget.ContentChange{ 1672 After: filepath.Join(s.dir, "foo"), 1673 }}, 1674 // in the preserve list but not present 1675 {"bar-name", &gadget.ContentChange{ 1676 After: filepath.Join(s.dir, "bar"), 1677 }}, 1678 // boot-assets/ -> / 1679 {"nested-dir/more-nested/identical", &gadget.ContentChange{ 1680 After: filepath.Join(s.dir, "boot-assets/nested-dir/more-nested/identical"), 1681 }}, 1682 {"nested-dir/more-nested/more", &gadget.ContentChange{ 1683 After: filepath.Join(s.dir, "boot-assets/nested-dir/more-nested/more"), 1684 }}, 1685 {"nested-dir/nested", &gadget.ContentChange{ 1686 After: filepath.Join(s.dir, "boot-assets/nested-dir/nested"), 1687 }}, 1688 {"nested-dir/same-as-target-dir/identical", &gadget.ContentChange{ 1689 After: filepath.Join(s.dir, "boot-assets/nested-dir/same-as-target-dir/identical"), 1690 }}, 1691 // in the preserve list but not present 1692 {"some-dir/data", &gadget.ContentChange{ 1693 After: filepath.Join(s.dir, "boot-assets/some-dir/data"), 1694 }}, 1695 {"some-dir/empty-file", &gadget.ContentChange{ 1696 After: filepath.Join(s.dir, "boot-assets/some-dir/empty-file"), 1697 }}, 1698 {"splash", &gadget.ContentChange{ 1699 After: filepath.Join(s.dir, "boot-assets/splash"), 1700 }}, 1701 // boot-assets/nested-dir/ -> /nested-copy/ 1702 {"nested-copy/more-nested/more", &gadget.ContentChange{ 1703 After: filepath.Join(s.dir, "boot-assets/nested-dir/more-nested/more"), 1704 }}, 1705 // boot-assets -> /boot-assets-copy/ 1706 {"boot-assets-copy/boot-assets/nested-dir/more-nested/identical", &gadget.ContentChange{ 1707 After: filepath.Join(s.dir, "boot-assets/nested-dir/more-nested/identical")}}, 1708 {"boot-assets-copy/boot-assets/nested-dir/more-nested/more", &gadget.ContentChange{ 1709 After: filepath.Join(s.dir, "boot-assets/nested-dir/more-nested/more")}}, 1710 {"boot-assets-copy/boot-assets/nested-dir/nested", &gadget.ContentChange{ 1711 After: filepath.Join(s.dir, "boot-assets/nested-dir/nested"), 1712 }}, 1713 {"boot-assets-copy/boot-assets/nested-dir/same-as-target-dir/identical", &gadget.ContentChange{ 1714 After: filepath.Join(s.dir, "boot-assets/nested-dir/same-as-target-dir/identical"), 1715 }}, 1716 {"boot-assets-copy/boot-assets/some-dir/data", &gadget.ContentChange{ 1717 After: filepath.Join(s.dir, "boot-assets/some-dir/data"), 1718 }}, 1719 {"boot-assets-copy/boot-assets/some-dir/empty-file", &gadget.ContentChange{ 1720 After: filepath.Join(s.dir, "boot-assets/some-dir/empty-file"), 1721 }}, 1722 {"boot-assets-copy/boot-assets/splash", &gadget.ContentChange{ 1723 After: filepath.Join(s.dir, "boot-assets/splash"), 1724 }}, 1725 }, 1726 }) 1727 1728 err = rw.Update() 1729 c.Assert(err, IsNil) 1730 1731 // files that existed were preserved 1732 for _, en := range preserve { 1733 p := filepath.Join(outDir, en) 1734 c.Check(p, testutil.FileEquals, "can't touch this") 1735 } 1736 // everything else was written 1737 verifyWrittenGadgetData(c, outDir, append(gdWritten, gdIdentical...)) 1738 } 1739 1740 func (s *mountedfilesystemTestSuite) TestMountedUpdaterDirContents(c *C) { 1741 // some data for the gadget 1742 gdWritten := []gadgetData{ 1743 {name: "bar/foo", target: "/bar-name/foo", content: "data"}, 1744 {name: "bar/nested/foo", target: "/bar-name/nested/foo", content: "data"}, 1745 {name: "bar/foo", target: "/bar-copy/bar/foo", content: "data"}, 1746 {name: "bar/nested/foo", target: "/bar-copy/bar/nested/foo", content: "data"}, 1747 {name: "deep-nested", target: "/this/is/some/deep/nesting/deep-nested", content: "data"}, 1748 } 1749 makeGadgetData(c, s.dir, gdWritten) 1750 1751 outDir := filepath.Join(c.MkDir(), "out-dir") 1752 1753 ps := &gadget.LaidOutStructure{ 1754 VolumeStructure: &gadget.VolumeStructure{ 1755 Size: 2048, 1756 Filesystem: "ext4", 1757 Content: []gadget.VolumeContent{ 1758 { 1759 // contents of bar under /bar-name/ 1760 Source: "bar/", 1761 Target: "/bar-name", 1762 }, { 1763 // whole bar under /bar-copy/ 1764 Source: "bar", 1765 Target: "/bar-copy/", 1766 }, { 1767 // deep prefix 1768 Source: "deep-nested", 1769 Target: "/this/is/some/deep/nesting/", 1770 }, 1771 }, 1772 Update: gadget.VolumeUpdate{ 1773 Edition: 1, 1774 }, 1775 }, 1776 } 1777 1778 rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) { 1779 c.Check(to, DeepEquals, ps) 1780 return outDir, nil 1781 }, nil) 1782 c.Assert(err, IsNil) 1783 c.Assert(rw, NotNil) 1784 1785 err = rw.Backup() 1786 c.Assert(err, IsNil) 1787 1788 err = rw.Update() 1789 c.Assert(err, IsNil) 1790 1791 verifyWrittenGadgetData(c, outDir, gdWritten) 1792 } 1793 1794 func (s *mountedfilesystemTestSuite) TestMountedUpdaterExpectsBackup(c *C) { 1795 // some data for the gadget 1796 gd := []gadgetData{ 1797 {name: "bar", target: "foo", content: "update"}, 1798 {name: "bar", target: "some-dir/foo", content: "update"}, 1799 } 1800 makeGadgetData(c, s.dir, gd) 1801 1802 outDir := filepath.Join(c.MkDir(), "out-dir") 1803 makeExistingData(c, outDir, []gadgetData{ 1804 {target: "foo", content: "content"}, 1805 {target: "some-dir/foo", content: "content"}, 1806 {target: "/preserved", content: "preserve"}, 1807 }) 1808 1809 ps := &gadget.LaidOutStructure{ 1810 VolumeStructure: &gadget.VolumeStructure{ 1811 Size: 2048, 1812 Filesystem: "ext4", 1813 Content: []gadget.VolumeContent{ 1814 { 1815 Source: "bar", 1816 Target: "/foo", 1817 }, { 1818 Source: "bar", 1819 Target: "/some-dir/foo", 1820 }, { 1821 Source: "bar", 1822 Target: "/preserved", 1823 }, 1824 }, 1825 Update: gadget.VolumeUpdate{ 1826 Edition: 1, 1827 // bar not in preserved files 1828 Preserve: []string{"preserved"}, 1829 }, 1830 }, 1831 } 1832 1833 rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) { 1834 c.Check(to, DeepEquals, ps) 1835 return outDir, nil 1836 }, nil) 1837 c.Assert(err, IsNil) 1838 c.Assert(rw, NotNil) 1839 1840 err = rw.Update() 1841 c.Assert(err, ErrorMatches, `cannot update content: missing backup file ".*/struct-0/foo.backup" for /foo`) 1842 // create a mock backup of first file 1843 makeSizedFile(c, filepath.Join(s.backup, "struct-0/foo.backup"), 0, nil) 1844 // try again 1845 err = rw.Update() 1846 c.Assert(err, ErrorMatches, `cannot update content: missing backup file ".*/struct-0/some-dir/foo.backup" for /some-dir/foo`) 1847 // create a mock backup of second entry 1848 makeSizedFile(c, filepath.Join(s.backup, "struct-0/some-dir/foo.backup"), 0, nil) 1849 // try again (preserved files need no backup) 1850 err = rw.Update() 1851 c.Assert(err, IsNil) 1852 1853 verifyWrittenGadgetData(c, outDir, []gadgetData{ 1854 {target: "foo", content: "update"}, 1855 {target: "some-dir/foo", content: "update"}, 1856 {target: "/preserved", content: "preserve"}, 1857 }) 1858 } 1859 1860 func (s *mountedfilesystemTestSuite) TestMountedUpdaterEmptyDir(c *C) { 1861 // some data for the gadget 1862 err := os.MkdirAll(filepath.Join(s.dir, "empty-dir"), 0755) 1863 c.Assert(err, IsNil) 1864 err = os.MkdirAll(filepath.Join(s.dir, "non-empty/empty-dir"), 0755) 1865 c.Assert(err, IsNil) 1866 1867 outDir := filepath.Join(c.MkDir(), "out-dir") 1868 1869 ps := &gadget.LaidOutStructure{ 1870 VolumeStructure: &gadget.VolumeStructure{ 1871 Size: 2048, 1872 Filesystem: "ext4", 1873 Content: []gadget.VolumeContent{ 1874 { 1875 Source: "/", 1876 Target: "/", 1877 }, { 1878 Source: "/", 1879 Target: "/foo", 1880 }, { 1881 Source: "/non-empty/empty-dir/", 1882 Target: "/contents-of-empty/", 1883 }, 1884 }, 1885 Update: gadget.VolumeUpdate{ 1886 Edition: 1, 1887 }, 1888 }, 1889 } 1890 1891 rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) { 1892 c.Check(to, DeepEquals, ps) 1893 return outDir, nil 1894 }, nil) 1895 c.Assert(err, IsNil) 1896 c.Assert(rw, NotNil) 1897 1898 err = rw.Update() 1899 c.Assert(err, Equals, gadget.ErrNoUpdate) 1900 1901 verifyDirContents(c, outDir, map[string]contentType{ 1902 // / -> / 1903 "empty-dir": typeDir, 1904 "non-empty/empty-dir": typeDir, 1905 1906 // / -> /foo 1907 "foo/empty-dir": typeDir, 1908 "foo/non-empty/empty-dir": typeDir, 1909 1910 // /non-empty/empty-dir/ -> /contents-of-empty/ 1911 "contents-of-empty": typeDir, 1912 }) 1913 } 1914 1915 func (s *mountedfilesystemTestSuite) TestMountedUpdaterSameFileSkipped(c *C) { 1916 // some data for the gadget 1917 gd := []gadgetData{ 1918 {name: "bar", target: "foo", content: "data"}, 1919 {name: "bar", target: "some-dir/foo", content: "data"}, 1920 } 1921 makeGadgetData(c, s.dir, gd) 1922 1923 outDir := filepath.Join(c.MkDir(), "out-dir") 1924 makeExistingData(c, outDir, []gadgetData{ 1925 {target: "foo", content: "same"}, 1926 {target: "some-dir/foo", content: "same"}, 1927 }) 1928 1929 ps := &gadget.LaidOutStructure{ 1930 VolumeStructure: &gadget.VolumeStructure{ 1931 Size: 2048, 1932 Filesystem: "ext4", 1933 Content: []gadget.VolumeContent{ 1934 { 1935 Source: "bar", 1936 Target: "/foo", 1937 }, { 1938 Source: "bar", 1939 Target: "/some-dir/foo", 1940 }, 1941 }, 1942 Update: gadget.VolumeUpdate{ 1943 Edition: 1, 1944 }, 1945 }, 1946 } 1947 1948 rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) { 1949 c.Check(to, DeepEquals, ps) 1950 return outDir, nil 1951 }, nil) 1952 c.Assert(err, IsNil) 1953 c.Assert(rw, NotNil) 1954 1955 // pretend a backup pass ran and found the files identical 1956 makeSizedFile(c, filepath.Join(s.backup, "struct-0/foo.same"), 0, nil) 1957 makeSizedFile(c, filepath.Join(s.backup, "struct-0/some-dir/foo.same"), 0, nil) 1958 1959 err = rw.Update() 1960 c.Assert(err, Equals, gadget.ErrNoUpdate) 1961 // files were not modified 1962 verifyWrittenGadgetData(c, outDir, []gadgetData{ 1963 {target: "foo", content: "same"}, 1964 {target: "some-dir/foo", content: "same"}, 1965 }) 1966 } 1967 1968 func (s *mountedfilesystemTestSuite) TestMountedUpdaterLonePrefix(c *C) { 1969 // some data for the gadget 1970 gd := []gadgetData{ 1971 {name: "bar", target: "1/nested/bar", content: "data"}, 1972 {name: "bar", target: "2/nested/foo", content: "data"}, 1973 {name: "bar", target: "3/nested/bar", content: "data"}, 1974 } 1975 makeGadgetData(c, s.dir, gd) 1976 1977 outDir := filepath.Join(c.MkDir(), "out-dir") 1978 1979 ps := &gadget.LaidOutStructure{ 1980 VolumeStructure: &gadget.VolumeStructure{ 1981 Size: 2048, 1982 Filesystem: "ext4", 1983 Content: []gadget.VolumeContent{ 1984 { 1985 Source: "bar", 1986 Target: "/1/nested/", 1987 }, { 1988 Source: "bar", 1989 Target: "/2/nested/foo", 1990 }, { 1991 Source: "/", 1992 Target: "/3/nested/", 1993 }, 1994 }, 1995 Update: gadget.VolumeUpdate{ 1996 Edition: 1, 1997 }, 1998 }, 1999 } 2000 2001 rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) { 2002 c.Check(to, DeepEquals, ps) 2003 return outDir, nil 2004 }, nil) 2005 c.Assert(err, IsNil) 2006 c.Assert(rw, NotNil) 2007 2008 err = rw.Update() 2009 c.Assert(err, IsNil) 2010 verifyWrittenGadgetData(c, outDir, gd) 2011 } 2012 2013 func (s *mountedfilesystemTestSuite) TestMountedUpdaterUpdateErrorOnSymlinkToFile(c *C) { 2014 gdWritten := []gadgetData{ 2015 {name: "data", target: "data", content: "some data"}, 2016 {name: "foo", symlinkTo: "data"}, 2017 } 2018 makeGadgetData(c, s.dir, gdWritten) 2019 2020 outDir := filepath.Join(c.MkDir(), "out-dir") 2021 2022 existing := []gadgetData{ 2023 {target: "data", content: "some data"}, 2024 } 2025 makeExistingData(c, outDir, existing) 2026 2027 ps := &gadget.LaidOutStructure{ 2028 VolumeStructure: &gadget.VolumeStructure{ 2029 Size: 2048, 2030 Filesystem: "ext4", 2031 Content: []gadget.VolumeContent{ 2032 {Source: "/", Target: "/"}, 2033 }, 2034 }, 2035 } 2036 2037 rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) { 2038 c.Check(to, DeepEquals, ps) 2039 return outDir, nil 2040 }, nil) 2041 c.Assert(err, IsNil) 2042 c.Assert(rw, NotNil) 2043 2044 // create a mock backup of first file 2045 makeSizedFile(c, filepath.Join(s.backup, "struct-0/data.backup"), 0, nil) 2046 2047 err = rw.Update() 2048 c.Assert(err, ErrorMatches, "cannot update content: cannot update file /foo: symbolic links are not supported") 2049 } 2050 2051 func (s *mountedfilesystemTestSuite) TestMountedUpdaterBackupErrorOnSymlinkToDir(c *C) { 2052 gd := []gadgetData{ 2053 {name: "bar/data", target: "bar/data", content: "some data"}, 2054 {name: "baz", symlinkTo: "bar"}, 2055 } 2056 makeGadgetData(c, s.dir, gd) 2057 2058 outDir := filepath.Join(c.MkDir(), "out-dir") 2059 2060 existing := []gadgetData{ 2061 {target: "bar/data", content: "some data"}, 2062 } 2063 makeExistingData(c, outDir, existing) 2064 2065 ps := &gadget.LaidOutStructure{ 2066 VolumeStructure: &gadget.VolumeStructure{ 2067 Size: 2048, 2068 Filesystem: "ext4", 2069 Content: []gadget.VolumeContent{ 2070 {Source: "/", Target: "/"}, 2071 }, 2072 }, 2073 } 2074 2075 rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) { 2076 c.Check(to, DeepEquals, ps) 2077 return outDir, nil 2078 }, nil) 2079 c.Assert(err, IsNil) 2080 c.Assert(rw, NotNil) 2081 2082 // create a mock backup of first file 2083 makeSizedFile(c, filepath.Join(s.backup, "struct-0/bar/data.backup"), 0, nil) 2084 makeSizedFile(c, filepath.Join(s.backup, "struct-0/bar.backup"), 0, nil) 2085 2086 err = rw.Update() 2087 c.Assert(err, ErrorMatches, "cannot update content: cannot update file /baz: symbolic links are not supported") 2088 } 2089 2090 func (s *mountedfilesystemTestSuite) TestMountedUpdaterRollbackFromBackup(c *C) { 2091 // some data for the gadget 2092 gd := []gadgetData{ 2093 {name: "bar", target: "foo", content: "data"}, 2094 {name: "bar", target: "some-dir/foo", content: "data"}, 2095 } 2096 makeGadgetData(c, s.dir, gd) 2097 2098 outDir := filepath.Join(c.MkDir(), "out-dir") 2099 makeExistingData(c, outDir, []gadgetData{ 2100 {target: "foo", content: "written"}, 2101 {target: "some-dir/foo", content: "written"}, 2102 }) 2103 2104 ps := &gadget.LaidOutStructure{ 2105 VolumeStructure: &gadget.VolumeStructure{ 2106 Size: 2048, 2107 Filesystem: "ext4", 2108 Content: []gadget.VolumeContent{ 2109 { 2110 Source: "bar", 2111 Target: "/foo", 2112 }, { 2113 Source: "bar", 2114 Target: "/some-dir/foo", 2115 }, 2116 }, 2117 Update: gadget.VolumeUpdate{ 2118 Edition: 1, 2119 }, 2120 }, 2121 } 2122 2123 muo := &mockContentUpdateObserver{ 2124 c: c, 2125 expectedStruct: ps, 2126 } 2127 rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) { 2128 c.Check(to, DeepEquals, ps) 2129 return outDir, nil 2130 }, muo) 2131 c.Assert(err, IsNil) 2132 c.Assert(rw, NotNil) 2133 2134 // pretend a backup pass ran and created a backup 2135 makeSizedFile(c, filepath.Join(s.backup, "struct-0/foo.backup"), 0, []byte("backup")) 2136 makeSizedFile(c, filepath.Join(s.backup, "struct-0/some-dir/foo.backup"), 0, []byte("backup")) 2137 2138 err = rw.Rollback() 2139 c.Assert(err, IsNil) 2140 // files were restored from backup 2141 verifyWrittenGadgetData(c, outDir, []gadgetData{ 2142 {target: "foo", content: "backup"}, 2143 {target: "some-dir/foo", content: "backup"}, 2144 }) 2145 2146 // only notified about content getting updated 2147 c.Check(muo.contentRollback, DeepEquals, map[string][]*mockContentChange{ 2148 outDir: { 2149 // rollback restores from the backups 2150 {"foo", &gadget.ContentChange{ 2151 After: filepath.Join(s.dir, "bar"), 2152 Before: filepath.Join(s.backup, "struct-0/foo.backup"), 2153 }}, 2154 {"some-dir/foo", &gadget.ContentChange{ 2155 After: filepath.Join(s.dir, "bar"), 2156 Before: filepath.Join(s.backup, "struct-0/some-dir/foo.backup"), 2157 }}, 2158 }, 2159 }) 2160 } 2161 2162 func (s *mountedfilesystemTestSuite) TestMountedUpdaterRollbackSkipSame(c *C) { 2163 // some data for the gadget 2164 gd := []gadgetData{ 2165 {name: "bar", content: "data"}, 2166 } 2167 makeGadgetData(c, s.dir, gd) 2168 2169 outDir := filepath.Join(c.MkDir(), "out-dir") 2170 makeExistingData(c, outDir, []gadgetData{ 2171 {target: "foo", content: "same"}, 2172 }) 2173 2174 ps := &gadget.LaidOutStructure{ 2175 VolumeStructure: &gadget.VolumeStructure{ 2176 Size: 2048, 2177 Filesystem: "ext4", 2178 Content: []gadget.VolumeContent{ 2179 { 2180 Source: "bar", 2181 Target: "/foo", 2182 }, 2183 }, 2184 Update: gadget.VolumeUpdate{ 2185 Edition: 1, 2186 }, 2187 }, 2188 } 2189 2190 muo := &mockContentUpdateObserver{ 2191 c: c, 2192 expectedStruct: ps, 2193 } 2194 rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) { 2195 c.Check(to, DeepEquals, ps) 2196 return outDir, nil 2197 }, muo) 2198 c.Assert(err, IsNil) 2199 c.Assert(rw, NotNil) 2200 2201 // pretend a backup pass ran and created a backup 2202 makeSizedFile(c, filepath.Join(s.backup, "struct-0/foo.same"), 0, nil) 2203 2204 err = rw.Rollback() 2205 c.Assert(err, IsNil) 2206 // files were not modified 2207 verifyWrittenGadgetData(c, outDir, []gadgetData{ 2208 {target: "foo", content: "same"}, 2209 }) 2210 // identical content did not need a rollback, no notifications 2211 c.Check(muo.contentRollback, HasLen, 0) 2212 } 2213 2214 func (s *mountedfilesystemTestSuite) TestMountedUpdaterRollbackSkipPreserved(c *C) { 2215 // some data for the gadget 2216 gd := []gadgetData{ 2217 {name: "bar", content: "data"}, 2218 } 2219 makeGadgetData(c, s.dir, gd) 2220 2221 outDir := filepath.Join(c.MkDir(), "out-dir") 2222 makeExistingData(c, outDir, []gadgetData{ 2223 {target: "foo", content: "preserved"}, 2224 }) 2225 2226 ps := &gadget.LaidOutStructure{ 2227 VolumeStructure: &gadget.VolumeStructure{ 2228 Size: 2048, 2229 Filesystem: "ext4", 2230 Content: []gadget.VolumeContent{ 2231 { 2232 Source: "bar", 2233 Target: "/foo", 2234 }, 2235 }, 2236 Update: gadget.VolumeUpdate{ 2237 Edition: 1, 2238 Preserve: []string{"foo"}, 2239 }, 2240 }, 2241 } 2242 2243 muo := &mockContentUpdateObserver{ 2244 c: c, 2245 expectedStruct: ps, 2246 } 2247 rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) { 2248 c.Check(to, DeepEquals, ps) 2249 return outDir, nil 2250 }, muo) 2251 c.Assert(err, IsNil) 2252 c.Assert(rw, NotNil) 2253 2254 // preserved files get no backup, but gets a stamp instead 2255 makeSizedFile(c, filepath.Join(s.backup, "struct-0/foo.preserve"), 0, nil) 2256 2257 err = rw.Rollback() 2258 c.Assert(err, IsNil) 2259 // files were not modified 2260 verifyWrittenGadgetData(c, outDir, []gadgetData{ 2261 {target: "foo", content: "preserved"}, 2262 }) 2263 // preserved content did not need a rollback, no notifications 2264 c.Check(muo.contentRollback, HasLen, 0) 2265 } 2266 2267 func (s *mountedfilesystemTestSuite) TestMountedUpdaterRollbackNewFiles(c *C) { 2268 makeGadgetData(c, s.dir, []gadgetData{ 2269 {name: "bar", content: "data"}, 2270 }) 2271 2272 outDir := filepath.Join(c.MkDir(), "out-dir") 2273 makeExistingData(c, outDir, []gadgetData{ 2274 {target: "foo", content: "written"}, 2275 {target: "some-dir/bar", content: "written"}, 2276 {target: "this/is/some/deep/nesting/bar", content: "written"}, 2277 }) 2278 2279 ps := &gadget.LaidOutStructure{ 2280 VolumeStructure: &gadget.VolumeStructure{ 2281 Size: 2048, 2282 Filesystem: "ext4", 2283 Content: []gadget.VolumeContent{ 2284 { 2285 Source: "bar", 2286 Target: "/foo", 2287 }, { 2288 Source: "bar", 2289 Target: "some-dir/", 2290 }, { 2291 Source: "bar", 2292 Target: "/this/is/some/deep/nesting/", 2293 }, 2294 }, 2295 Update: gadget.VolumeUpdate{ 2296 Edition: 1, 2297 }, 2298 }, 2299 } 2300 2301 muo := &mockContentUpdateObserver{ 2302 c: c, 2303 expectedStruct: ps, 2304 } 2305 rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) { 2306 c.Check(to, DeepEquals, ps) 2307 return outDir, nil 2308 }, muo) 2309 c.Assert(err, IsNil) 2310 c.Assert(rw, NotNil) 2311 2312 // none of the marker files exists, files are new, will be removed 2313 err = rw.Rollback() 2314 c.Assert(err, IsNil) 2315 // everything was removed 2316 verifyDirContents(c, outDir, map[string]contentType{}) 2317 // new files were rolled back 2318 c.Check(muo.contentRollback, DeepEquals, map[string][]*mockContentChange{ 2319 outDir: { 2320 // files did not exist, so there was no 'before' content 2321 {"foo", &gadget.ContentChange{ 2322 After: filepath.Join(s.dir, "bar"), 2323 Before: "", 2324 }}, 2325 {"some-dir/bar", &gadget.ContentChange{ 2326 After: filepath.Join(s.dir, "bar"), 2327 Before: "", 2328 }}, 2329 {"this/is/some/deep/nesting/bar", &gadget.ContentChange{ 2330 After: filepath.Join(s.dir, "bar"), 2331 Before: "", 2332 }}, 2333 }, 2334 }) 2335 } 2336 2337 func (s *mountedfilesystemTestSuite) TestMountedUpdaterRollbackRestoreFails(c *C) { 2338 makeGadgetData(c, s.dir, []gadgetData{ 2339 {name: "bar", content: "data"}, 2340 }) 2341 2342 outDir := filepath.Join(c.MkDir(), "out-dir") 2343 makeExistingData(c, outDir, []gadgetData{ 2344 {target: "foo", content: "written"}, 2345 {target: "some-dir/foo", content: "written"}, 2346 }) 2347 // the file exists, and cannot be modified directly, rollback will still 2348 // restore the backup as we atomically swap copies with rename() 2349 err := os.Chmod(filepath.Join(outDir, "foo"), 0000) 2350 c.Assert(err, IsNil) 2351 2352 ps := &gadget.LaidOutStructure{ 2353 VolumeStructure: &gadget.VolumeStructure{ 2354 Size: 2048, 2355 Filesystem: "ext4", 2356 Content: []gadget.VolumeContent{ 2357 { 2358 Source: "bar", 2359 Target: "/foo", 2360 }, { 2361 Source: "bar", 2362 Target: "/some-dir/foo", 2363 }, 2364 }, 2365 Update: gadget.VolumeUpdate{ 2366 Edition: 1, 2367 }, 2368 }, 2369 } 2370 2371 rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) { 2372 c.Check(to, DeepEquals, ps) 2373 return outDir, nil 2374 }, nil) 2375 c.Assert(err, IsNil) 2376 c.Assert(rw, NotNil) 2377 2378 // one file backed up, the other is new 2379 makeSizedFile(c, filepath.Join(s.backup, "struct-0/foo.backup"), 0, []byte("backup")) 2380 2381 err = rw.Rollback() 2382 c.Assert(err, IsNil) 2383 // the file was restored 2384 c.Check(filepath.Join(outDir, "foo"), testutil.FileEquals, "backup") 2385 // directory was removed 2386 c.Check(osutil.IsDirectory(filepath.Join(outDir, "some-dir")), Equals, false) 2387 2388 // mock the data again 2389 makeExistingData(c, outDir, []gadgetData{ 2390 {target: "foo", content: "written"}, 2391 {target: "some-dir/foo", content: "written"}, 2392 }) 2393 2394 // make the directory non-writable 2395 err = os.Chmod(filepath.Join(outDir, "some-dir"), 0555) 2396 c.Assert(err, IsNil) 2397 // restore permissions later, otherwise test suite cleanup complains 2398 defer os.Chmod(filepath.Join(outDir, "some-dir"), 0755) 2399 2400 err = rw.Rollback() 2401 c.Assert(err, ErrorMatches, "cannot rollback content: cannot remove written update: remove .*/out-dir/some-dir/foo: permission denied") 2402 } 2403 2404 func (s *mountedfilesystemTestSuite) TestMountedUpdaterRollbackNotWritten(c *C) { 2405 makeGadgetData(c, s.dir, []gadgetData{ 2406 {name: "bar", content: "data"}, 2407 }) 2408 2409 outDir := filepath.Join(c.MkDir(), "out-dir") 2410 2411 ps := &gadget.LaidOutStructure{ 2412 VolumeStructure: &gadget.VolumeStructure{ 2413 Size: 2048, 2414 Filesystem: "ext4", 2415 Content: []gadget.VolumeContent{ 2416 { 2417 Source: "bar", 2418 Target: "/foo", 2419 }, { 2420 Source: "bar", 2421 Target: "/some-dir/foo", 2422 }, 2423 }, 2424 Update: gadget.VolumeUpdate{ 2425 Edition: 1, 2426 }, 2427 }, 2428 } 2429 2430 muo := &mockContentUpdateObserver{ 2431 c: c, 2432 expectedStruct: ps, 2433 } 2434 rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) { 2435 c.Check(to, DeepEquals, ps) 2436 return outDir, nil 2437 }, muo) 2438 c.Assert(err, IsNil) 2439 c.Assert(rw, NotNil) 2440 2441 // rollback does not error out if files were not written 2442 err = rw.Rollback() 2443 c.Assert(err, IsNil) 2444 // observer would be notified that files were to be written, and so it 2445 // must be notified when they would be rolled back 2446 c.Check(muo.contentRollback, DeepEquals, map[string][]*mockContentChange{ 2447 outDir: { 2448 // rollback restores from the backups 2449 {"foo", &gadget.ContentChange{ 2450 After: filepath.Join(s.dir, "bar"), 2451 }}, 2452 {"some-dir/foo", &gadget.ContentChange{ 2453 After: filepath.Join(s.dir, "bar"), 2454 }}, 2455 }, 2456 }) 2457 } 2458 2459 func (s *mountedfilesystemTestSuite) TestMountedUpdaterRollbackDirectory(c *C) { 2460 makeGadgetData(c, s.dir, []gadgetData{ 2461 {name: "some-dir/bar", content: "data"}, 2462 {name: "some-dir/foo", content: "data"}, 2463 {name: "some-dir/nested/nested-foo", content: "data"}, 2464 {name: "empty-dir/"}, 2465 {name: "bar", content: "data"}, 2466 }) 2467 2468 outDir := filepath.Join(c.MkDir(), "out-dir") 2469 makeExistingData(c, outDir, []gadgetData{ 2470 // some-dir/ -> / 2471 {target: "foo", content: "written"}, 2472 {target: "bar", content: "written"}, 2473 {target: "nested/nested-foo", content: "written"}, 2474 // some-dir/ -> /other-dir/ 2475 {target: "other-dir/foo", content: "written"}, 2476 {target: "other-dir/bar", content: "written"}, 2477 {target: "other-dir/nested/nested-foo", content: "written"}, 2478 // some-dir/nested -> /other-dir/nested/ 2479 {target: "other-dir/nested/nested/nested-foo", content: "written"}, 2480 // bar -> /this/is/some/deep/nesting/ 2481 {target: "this/is/some/deep/nesting/bar", content: "written"}, 2482 {target: "lone-dir/"}, 2483 }) 2484 2485 ps := &gadget.LaidOutStructure{ 2486 VolumeStructure: &gadget.VolumeStructure{ 2487 Size: 2048, 2488 Filesystem: "ext4", 2489 Content: []gadget.VolumeContent{ 2490 { 2491 Source: "some-dir/", 2492 Target: "/", 2493 }, { 2494 Source: "some-dir/", 2495 Target: "/other-dir/", 2496 }, { 2497 Source: "some-dir/nested", 2498 Target: "/other-dir/nested/", 2499 }, { 2500 Source: "bar", 2501 Target: "/this/is/some/deep/nesting/", 2502 }, { 2503 Source: "empty-dir/", 2504 Target: "/lone-dir/", 2505 }, 2506 }, 2507 Update: gadget.VolumeUpdate{ 2508 Edition: 1, 2509 }, 2510 }, 2511 } 2512 2513 muo := &mockContentUpdateObserver{ 2514 c: c, 2515 expectedStruct: ps, 2516 } 2517 rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) { 2518 c.Check(to, DeepEquals, ps) 2519 return outDir, nil 2520 }, muo) 2521 c.Assert(err, IsNil) 2522 c.Assert(rw, NotNil) 2523 2524 // one file backed up 2525 makeSizedFile(c, filepath.Join(s.backup, "struct-0/foo.backup"), 0, []byte("backup")) 2526 // pretend part of the directory structure existed before 2527 makeSizedFile(c, filepath.Join(s.backup, "struct-0/this/is/some.backup"), 0, nil) 2528 makeSizedFile(c, filepath.Join(s.backup, "struct-0/this/is.backup"), 0, nil) 2529 makeSizedFile(c, filepath.Join(s.backup, "struct-0/this.backup"), 0, nil) 2530 makeSizedFile(c, filepath.Join(s.backup, "struct-0/lone-dir.backup"), 0, nil) 2531 2532 // files without a marker are new, will be removed 2533 err = rw.Rollback() 2534 c.Assert(err, IsNil) 2535 2536 verifyDirContents(c, outDir, map[string]contentType{ 2537 "lone-dir": typeDir, 2538 "this/is/some": typeDir, 2539 "foo": typeFile, 2540 }) 2541 // this one got restored 2542 c.Check(filepath.Join(outDir, "foo"), testutil.FileEquals, "backup") 2543 2544 c.Check(muo.contentRollback, DeepEquals, map[string][]*mockContentChange{ 2545 outDir: { 2546 // a new file 2547 {"bar", &gadget.ContentChange{ 2548 After: filepath.Join(s.dir, "some-dir/bar"), 2549 }}, 2550 // this file was restored from backup 2551 {"foo", &gadget.ContentChange{ 2552 After: filepath.Join(s.dir, "some-dir/foo"), 2553 Before: filepath.Join(s.backup, "struct-0/foo.backup"), 2554 }}, 2555 // new files till the end 2556 {"nested/nested-foo", &gadget.ContentChange{ 2557 After: filepath.Join(s.dir, "some-dir/nested/nested-foo"), 2558 }}, 2559 {"other-dir/bar", &gadget.ContentChange{ 2560 After: filepath.Join(s.dir, "some-dir/bar"), 2561 }}, 2562 {"other-dir/foo", &gadget.ContentChange{ 2563 After: filepath.Join(s.dir, "some-dir/foo"), 2564 }}, 2565 {"other-dir/nested/nested-foo", &gadget.ContentChange{ 2566 After: filepath.Join(s.dir, "some-dir/nested/nested-foo"), 2567 }}, 2568 {"other-dir/nested/nested/nested-foo", &gadget.ContentChange{ 2569 After: filepath.Join(s.dir, "some-dir/nested/nested-foo"), 2570 }}, 2571 {"this/is/some/deep/nesting/bar", &gadget.ContentChange{ 2572 After: filepath.Join(s.dir, "bar"), 2573 }}, 2574 }, 2575 }) 2576 } 2577 2578 func (s *mountedfilesystemTestSuite) TestMountedUpdaterEndToEndOne(c *C) { 2579 // some data for the gadget 2580 gdWritten := []gadgetData{ 2581 {name: "foo", target: "foo-dir/foo", content: "data"}, 2582 {name: "bar", target: "bar-name", content: "data"}, 2583 {name: "boot-assets/splash", target: "splash", content: "data"}, 2584 {name: "boot-assets/dtb", target: "dtb", content: "data"}, 2585 {name: "boot-assets/some-dir/data", target: "some-dir/data", content: "data"}, 2586 {name: "boot-assets/some-dir/empty-file", target: "some-dir/empty-file", content: ""}, 2587 {name: "boot-assets/nested-dir/more-nested/more", target: "/nested-copy/more-nested/more", content: "data"}, 2588 } 2589 gdNotWritten := []gadgetData{ 2590 {name: "foo", target: "/foo", content: "data"}, 2591 {name: "boot-assets/some-dir/data", target: "data-copy", content: "data"}, 2592 {name: "boot-assets/nested-dir/nested", target: "/nested-copy/nested", content: "data"}, 2593 {name: "preserved/same-content", target: "preserved/same-content", content: "can't touch this"}, 2594 } 2595 gdSameContent := []gadgetData{ 2596 {name: "foo", target: "/foo-same", content: "data"}, 2597 } 2598 makeGadgetData(c, s.dir, append(gdWritten, gdNotWritten...)) 2599 err := os.MkdirAll(filepath.Join(s.dir, "boot-assets/empty-dir"), 0755) 2600 c.Assert(err, IsNil) 2601 2602 outDir := filepath.Join(c.MkDir(), "out-dir") 2603 2604 makeExistingData(c, outDir, []gadgetData{ 2605 {target: "dtb", content: "updated"}, 2606 {target: "foo", content: "can't touch this"}, 2607 {target: "foo-same", content: "data"}, 2608 {target: "data-copy-preserved", content: "can't touch this"}, 2609 {target: "data-copy", content: "can't touch this"}, 2610 {target: "nested-copy/nested", content: "can't touch this"}, 2611 {target: "nested-copy/more-nested/"}, 2612 {target: "not-listed", content: "can't touch this"}, 2613 {target: "unrelated/data/here", content: "unrelated"}, 2614 {target: "preserved/same-content-for-list", content: "can't touch this"}, 2615 {target: "preserved/same-content-for-observer", content: "can't touch this"}, 2616 }) 2617 // these exist in the root directory and are preserved 2618 preserve := []string{ 2619 // mix entries with leading / and without 2620 "/foo", 2621 "/data-copy-preserved", 2622 "nested-copy/nested", 2623 "not-listed", // not present in 'gadget' contents 2624 "preserved/same-content-for-list", 2625 } 2626 // these are preserved, but don't exist in the root, so data from gadget 2627 // will be written 2628 preserveButNotPresent := []string{ 2629 "/bar-name", 2630 "some-dir/data", 2631 } 2632 2633 ps := &gadget.LaidOutStructure{ 2634 VolumeStructure: &gadget.VolumeStructure{ 2635 Size: 2048, 2636 Filesystem: "ext4", 2637 Content: []gadget.VolumeContent{ 2638 { 2639 Source: "foo", 2640 Target: "/foo-dir/", 2641 }, { 2642 // would overwrite /foo 2643 Source: "foo", 2644 Target: "/", 2645 }, { 2646 // nothing written, content is unchanged 2647 Source: "foo", 2648 Target: "/foo-same", 2649 }, { 2650 // preserved, but not present, will be 2651 // written 2652 Source: "bar", 2653 Target: "/bar-name", 2654 }, { 2655 // some-dir/data is preserved, but not 2656 // present, hence will be written 2657 Source: "boot-assets/", 2658 Target: "/", 2659 }, { 2660 // would overwrite /data-copy 2661 Source: "boot-assets/some-dir/data", 2662 Target: "/data-copy-preserved", 2663 }, { 2664 Source: "boot-assets/some-dir/data", 2665 Target: "/data-copy", 2666 }, { 2667 // would overwrite /nested-copy/nested 2668 Source: "boot-assets/nested-dir/", 2669 Target: "/nested-copy/", 2670 }, { 2671 Source: "boot-assets", 2672 Target: "/boot-assets-copy/", 2673 }, { 2674 Source: "/boot-assets/empty-dir/", 2675 Target: "/lone-dir/nested/", 2676 }, { 2677 Source: "preserved/same-content", 2678 Target: "preserved/same-content-for-list", 2679 }, { 2680 Source: "preserved/same-content", 2681 Target: "preserved/same-content-for-observer", 2682 }, 2683 }, 2684 Update: gadget.VolumeUpdate{ 2685 Edition: 1, 2686 Preserve: append(preserve, preserveButNotPresent...), 2687 }, 2688 }, 2689 } 2690 2691 muo := &mockContentUpdateObserver{ 2692 c: c, 2693 expectedStruct: ps, 2694 preserveTargets: []string{ 2695 "preserved/same-content-for-observer", 2696 }, 2697 } 2698 rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) { 2699 c.Check(to, DeepEquals, ps) 2700 return outDir, nil 2701 }, muo) 2702 c.Assert(err, IsNil) 2703 c.Assert(rw, NotNil) 2704 2705 originalState := map[string]contentType{ 2706 "foo": typeFile, 2707 "foo-same": typeFile, 2708 "dtb": typeFile, 2709 "data-copy": typeFile, 2710 "not-listed": typeFile, 2711 "data-copy-preserved": typeFile, 2712 "nested-copy/nested": typeFile, 2713 "nested-copy/more-nested": typeDir, 2714 "unrelated/data/here": typeFile, 2715 "preserved/same-content-for-list": typeFile, 2716 "preserved/same-content-for-observer": typeFile, 2717 } 2718 verifyDirContents(c, outDir, originalState) 2719 2720 // run the backup phase 2721 err = rw.Backup() 2722 c.Assert(err, IsNil) 2723 2724 verifyDirContents(c, filepath.Join(s.backup, "struct-0"), map[string]contentType{ 2725 "nested-copy.backup": typeFile, 2726 "nested-copy/nested.preserve": typeFile, 2727 "nested-copy/more-nested.backup": typeFile, 2728 "foo.preserve": typeFile, 2729 "foo-same.same": typeFile, 2730 "data-copy-preserved.preserve": typeFile, 2731 "data-copy.backup": typeFile, 2732 "dtb.backup": typeFile, 2733 "preserved.backup": typeFile, 2734 "preserved/same-content-for-list.preserve": typeFile, 2735 "preserved/same-content-for-observer.same": typeFile, 2736 }) 2737 2738 expectedObservedContentChange := map[string][]*mockContentChange{ 2739 // observer is notified about changed and new files 2740 outDir: { 2741 {"foo-dir/foo", &gadget.ContentChange{ 2742 After: filepath.Join(s.dir, "foo"), 2743 }}, 2744 {"bar-name", &gadget.ContentChange{ 2745 After: filepath.Join(s.dir, "bar"), 2746 }}, 2747 // update with changed content 2748 {"dtb", &gadget.ContentChange{ 2749 After: filepath.Join(s.dir, "boot-assets/dtb"), 2750 Before: filepath.Join(s.backup, "struct-0/dtb.backup"), 2751 }}, 2752 // new files 2753 {"nested-dir/more-nested/more", &gadget.ContentChange{ 2754 After: filepath.Join(s.dir, "boot-assets/nested-dir/more-nested/more"), 2755 }}, 2756 {"nested-dir/nested", &gadget.ContentChange{ 2757 After: filepath.Join(s.dir, "boot-assets/nested-dir/nested"), 2758 }}, 2759 {"some-dir/data", &gadget.ContentChange{ 2760 After: filepath.Join(s.dir, "boot-assets/some-dir/data"), 2761 }}, 2762 // new files 2763 {"some-dir/empty-file", &gadget.ContentChange{ 2764 After: filepath.Join(s.dir, "boot-assets/some-dir/empty-file"), 2765 }}, 2766 {"splash", &gadget.ContentChange{ 2767 After: filepath.Join(s.dir, "boot-assets/splash"), 2768 }}, 2769 // update with changed content 2770 {"data-copy", &gadget.ContentChange{ 2771 After: filepath.Join(s.dir, "boot-assets/some-dir/data"), 2772 Before: filepath.Join(s.backup, "struct-0/data-copy.backup"), 2773 }}, 2774 // new files 2775 {"nested-copy/more-nested/more", &gadget.ContentChange{ 2776 After: filepath.Join(s.dir, "boot-assets/nested-dir/more-nested/more"), 2777 }}, 2778 {"boot-assets-copy/boot-assets/dtb", &gadget.ContentChange{ 2779 After: filepath.Join(s.dir, "boot-assets/dtb"), 2780 }}, 2781 {"boot-assets-copy/boot-assets/nested-dir/more-nested/more", &gadget.ContentChange{ 2782 After: filepath.Join(s.dir, "boot-assets/nested-dir/more-nested/more")}, 2783 }, 2784 {"boot-assets-copy/boot-assets/nested-dir/nested", &gadget.ContentChange{ 2785 After: filepath.Join(s.dir, "boot-assets/nested-dir/nested"), 2786 }}, 2787 {"boot-assets-copy/boot-assets/some-dir/data", &gadget.ContentChange{ 2788 After: filepath.Join(s.dir, "boot-assets/some-dir/data"), 2789 }}, 2790 {"boot-assets-copy/boot-assets/some-dir/empty-file", &gadget.ContentChange{ 2791 After: filepath.Join(s.dir, "boot-assets/some-dir/empty-file"), 2792 }}, 2793 {"boot-assets-copy/boot-assets/splash", &gadget.ContentChange{ 2794 After: filepath.Join(s.dir, "boot-assets/splash"), 2795 }}, 2796 }, 2797 } 2798 2799 // observe calls happen in the order the structure content gets analyzed 2800 c.Check(muo.contentUpdate, DeepEquals, expectedObservedContentChange) 2801 2802 // run the update phase 2803 err = rw.Update() 2804 c.Assert(err, IsNil) 2805 2806 verifyDirContents(c, outDir, map[string]contentType{ 2807 "foo": typeFile, 2808 "foo-same": typeFile, 2809 "not-listed": typeFile, 2810 2811 // boot-assets/some-dir/data -> /data-copy 2812 "data-copy": typeFile, 2813 2814 // boot-assets/some-dir/data -> /data-copy-preserved 2815 "data-copy-preserved": typeFile, 2816 2817 // foo -> /foo-dir/ 2818 "foo-dir/foo": typeFile, 2819 2820 // bar -> /bar-name 2821 "bar-name": typeFile, 2822 2823 // boot-assets/ -> / 2824 "dtb": typeFile, 2825 "splash": typeFile, 2826 "some-dir/data": typeFile, 2827 "some-dir/empty-file": typeFile, 2828 "nested-dir/nested": typeFile, 2829 "nested-dir/more-nested/more": typeFile, 2830 "empty-dir": typeDir, 2831 2832 // boot-assets -> /boot-assets-copy/ 2833 "boot-assets-copy/boot-assets/dtb": typeFile, 2834 "boot-assets-copy/boot-assets/splash": typeFile, 2835 "boot-assets-copy/boot-assets/some-dir/data": typeFile, 2836 "boot-assets-copy/boot-assets/some-dir/empty-file": typeFile, 2837 "boot-assets-copy/boot-assets/nested-dir/nested": typeFile, 2838 "boot-assets-copy/boot-assets/nested-dir/more-nested/more": typeFile, 2839 "boot-assets-copy/boot-assets/empty-dir": typeDir, 2840 2841 // boot-assets/nested-dir/ -> /nested-copy/ 2842 "nested-copy/nested": typeFile, 2843 "nested-copy/more-nested/more": typeFile, 2844 2845 // data that was not part of the update 2846 "unrelated/data/here": typeFile, 2847 2848 // boot-assets/empty-dir/ -> /lone-dir/nested/ 2849 "lone-dir/nested": typeDir, 2850 2851 "preserved/same-content-for-list": typeFile, 2852 "preserved/same-content-for-observer": typeFile, 2853 }) 2854 2855 // files that existed were preserved 2856 for _, en := range preserve { 2857 p := filepath.Join(outDir, en) 2858 c.Check(p, testutil.FileEquals, "can't touch this") 2859 } 2860 // everything else was written 2861 verifyWrittenGadgetData(c, outDir, append(gdWritten, gdSameContent...)) 2862 2863 err = rw.Rollback() 2864 c.Assert(err, IsNil) 2865 // back to square one 2866 verifyDirContents(c, outDir, originalState) 2867 2868 c.Check(muo.contentRollback, DeepEquals, expectedObservedContentChange) 2869 // call rollback once more, we should observe the same files again 2870 muo.contentRollback = nil 2871 err = rw.Rollback() 2872 c.Assert(err, IsNil) 2873 c.Check(muo.contentRollback, DeepEquals, expectedObservedContentChange) 2874 // file contents are unchanged 2875 verifyDirContents(c, outDir, originalState) 2876 } 2877 2878 func (s *mountedfilesystemTestSuite) TestMountedUpdaterTrivialValidation(c *C) { 2879 psNoFs := &gadget.LaidOutStructure{ 2880 VolumeStructure: &gadget.VolumeStructure{ 2881 Size: 2048, 2882 // no filesystem 2883 Content: []gadget.VolumeContent{}, 2884 }, 2885 } 2886 2887 lookupFail := func(to *gadget.LaidOutStructure) (string, error) { 2888 c.Fatalf("unexpected call") 2889 return "", nil 2890 } 2891 2892 rw, err := gadget.NewMountedFilesystemUpdater(s.dir, psNoFs, s.backup, lookupFail, nil) 2893 c.Assert(err, ErrorMatches, "structure #0 has no filesystem") 2894 c.Assert(rw, IsNil) 2895 2896 ps := &gadget.LaidOutStructure{ 2897 VolumeStructure: &gadget.VolumeStructure{ 2898 Size: 2048, 2899 Filesystem: "ext4", 2900 Content: []gadget.VolumeContent{}, 2901 }, 2902 } 2903 2904 rw, err = gadget.NewMountedFilesystemUpdater("", ps, s.backup, lookupFail, nil) 2905 c.Assert(err, ErrorMatches, `internal error: gadget content directory cannot be unset`) 2906 c.Assert(rw, IsNil) 2907 2908 rw, err = gadget.NewMountedFilesystemUpdater(s.dir, ps, "", lookupFail, nil) 2909 c.Assert(err, ErrorMatches, `internal error: backup directory must not be unset`) 2910 c.Assert(rw, IsNil) 2911 2912 rw, err = gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, nil, nil) 2913 c.Assert(err, ErrorMatches, `internal error: mount lookup helper must be provided`) 2914 c.Assert(rw, IsNil) 2915 2916 rw, err = gadget.NewMountedFilesystemUpdater(s.dir, nil, s.backup, lookupFail, nil) 2917 c.Assert(err, ErrorMatches, `internal error: \*LaidOutStructure.*`) 2918 c.Assert(rw, IsNil) 2919 2920 lookupOk := func(to *gadget.LaidOutStructure) (string, error) { 2921 return filepath.Join(s.dir, "foobar"), nil 2922 } 2923 2924 for _, tc := range []struct { 2925 content gadget.VolumeContent 2926 match string 2927 }{ 2928 {content: gadget.VolumeContent{Source: "", Target: "/"}, match: "internal error: source cannot be unset"}, 2929 {content: gadget.VolumeContent{Source: "/", Target: ""}, match: "internal error: target cannot be unset"}, 2930 } { 2931 testPs := &gadget.LaidOutStructure{ 2932 VolumeStructure: &gadget.VolumeStructure{ 2933 Size: 2048, 2934 Filesystem: "ext4", 2935 Content: []gadget.VolumeContent{tc.content}, 2936 }, 2937 } 2938 2939 rw, err := gadget.NewMountedFilesystemUpdater(s.dir, testPs, s.backup, lookupOk, nil) 2940 c.Assert(err, IsNil) 2941 c.Assert(rw, NotNil) 2942 2943 err = rw.Update() 2944 c.Assert(err, ErrorMatches, "cannot update content: "+tc.match) 2945 2946 err = rw.Backup() 2947 c.Assert(err, ErrorMatches, "cannot backup content: "+tc.match) 2948 2949 err = rw.Rollback() 2950 c.Assert(err, ErrorMatches, "cannot rollback content: "+tc.match) 2951 } 2952 } 2953 2954 func (s *mountedfilesystemTestSuite) TestMountedUpdaterMountLookupFail(c *C) { 2955 ps := &gadget.LaidOutStructure{ 2956 VolumeStructure: &gadget.VolumeStructure{ 2957 Size: 2048, 2958 Filesystem: "ext4", 2959 Content: []gadget.VolumeContent{ 2960 {Source: "/", Target: "/"}, 2961 }, 2962 Update: gadget.VolumeUpdate{ 2963 Edition: 1, 2964 }, 2965 }, 2966 } 2967 2968 lookupFail := func(to *gadget.LaidOutStructure) (string, error) { 2969 c.Check(to, DeepEquals, ps) 2970 return "", errors.New("fail fail fail") 2971 } 2972 2973 rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, lookupFail, nil) 2974 c.Assert(err, ErrorMatches, "cannot find mount location of structure #0: fail fail fail") 2975 c.Assert(rw, IsNil) 2976 } 2977 2978 func (s *mountedfilesystemTestSuite) TestMountedUpdaterNonFilePreserveError(c *C) { 2979 // some data for the gadget 2980 gd := []gadgetData{ 2981 {name: "foo", content: "data"}, 2982 } 2983 makeGadgetData(c, s.dir, gd) 2984 2985 outDir := filepath.Join(c.MkDir(), "out-dir") 2986 // will conflict with preserve entry 2987 err := os.MkdirAll(filepath.Join(outDir, "foo"), 0755) 2988 c.Assert(err, IsNil) 2989 2990 ps := &gadget.LaidOutStructure{ 2991 VolumeStructure: &gadget.VolumeStructure{ 2992 Size: 2048, 2993 Filesystem: "ext4", 2994 Content: []gadget.VolumeContent{ 2995 {Source: "/", Target: "/"}, 2996 }, 2997 Update: gadget.VolumeUpdate{ 2998 Preserve: []string{"foo"}, 2999 Edition: 1, 3000 }, 3001 }, 3002 } 3003 3004 rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) { 3005 c.Check(to, DeepEquals, ps) 3006 return outDir, nil 3007 }, nil) 3008 c.Assert(err, IsNil) 3009 c.Assert(rw, NotNil) 3010 3011 err = rw.Backup() 3012 c.Check(err, ErrorMatches, `cannot map preserve entries for mount location ".*/out-dir": preserved entry "foo" cannot be a directory`) 3013 err = rw.Update() 3014 c.Check(err, ErrorMatches, `cannot map preserve entries for mount location ".*/out-dir": preserved entry "foo" cannot be a directory`) 3015 err = rw.Rollback() 3016 c.Check(err, ErrorMatches, `cannot map preserve entries for mount location ".*/out-dir": preserved entry "foo" cannot be a directory`) 3017 } 3018 3019 func (s *mountedfilesystemTestSuite) TestMountedUpdaterObserverPreservesBootAssets(c *C) { 3020 // mirror pc-amd64-gadget 3021 gd := []gadgetData{ 3022 {name: "grub.conf", content: "grub.conf from gadget"}, 3023 {name: "grubx64.efi", content: "grubx64.efi from gadget"}, 3024 {name: "foo", content: "foo from gadget"}, 3025 } 3026 makeGadgetData(c, s.dir, gd) 3027 3028 outDir := filepath.Join(c.MkDir(), "out-dir") 3029 3030 existingGrubCfg := `# Snapd-Boot-Config-Edition: 1 3031 managed grub.cfg from disk` 3032 makeExistingData(c, outDir, []gadgetData{ 3033 {target: "EFI/boot/grubx64.efi", content: "grubx64.efi from disk"}, 3034 {target: "EFI/ubuntu/grub.cfg", content: existingGrubCfg}, 3035 {target: "foo", content: "foo from disk"}, 3036 }) 3037 // based on pc gadget 3038 ps := &gadget.LaidOutStructure{ 3039 VolumeStructure: &gadget.VolumeStructure{ 3040 Size: 2048, 3041 Role: gadget.SystemBoot, 3042 Filesystem: "ext4", 3043 Content: []gadget.VolumeContent{ 3044 {Source: "grubx64.efi", Target: "EFI/boot/grubx64.efi"}, 3045 {Source: "grub.conf", Target: "EFI/ubuntu/grub.cfg"}, 3046 {Source: "foo", Target: "foo"}, 3047 }, 3048 Update: gadget.VolumeUpdate{ 3049 Preserve: []string{"foo"}, 3050 Edition: 1, 3051 }, 3052 }, 3053 } 3054 obs := &mockContentUpdateObserver{ 3055 c: c, 3056 expectedStruct: ps, 3057 preserveTargets: []string{"EFI/ubuntu/grub.cfg"}, 3058 } 3059 rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, 3060 func(to *gadget.LaidOutStructure) (string, error) { 3061 c.Check(to, DeepEquals, ps) 3062 return outDir, nil 3063 }, 3064 obs) 3065 c.Assert(err, IsNil) 3066 c.Assert(rw, NotNil) 3067 3068 expectedFileStamps := map[string]contentType{ 3069 "EFI.backup": typeFile, 3070 "EFI/boot/grubx64.efi.backup": typeFile, 3071 "EFI/boot.backup": typeFile, 3072 "EFI/ubuntu.backup": typeFile, 3073 3074 // listed explicitly in the structure 3075 "foo.preserve": typeFile, 3076 // requested by observer 3077 "EFI/ubuntu/grub.cfg.ignore": typeFile, 3078 } 3079 3080 for _, step := range []struct { 3081 name string 3082 call func() error 3083 }{ 3084 {name: "backup", call: rw.Backup}, 3085 {name: "update", call: rw.Update}, 3086 {name: "rollback", call: rw.Rollback}, 3087 } { 3088 c.Logf("step: %v", step.name) 3089 err := step.call() 3090 c.Assert(err, IsNil) 3091 3092 switch step.name { 3093 case "backup": 3094 c.Check(filepath.Join(outDir, "EFI/boot/grubx64.efi"), testutil.FileEquals, "grubx64.efi from disk") 3095 c.Check(filepath.Join(outDir, "EFI/ubuntu/grub.cfg"), testutil.FileEquals, existingGrubCfg) 3096 c.Check(filepath.Join(outDir, "foo"), testutil.FileEquals, "foo from disk") 3097 case "update": 3098 c.Check(filepath.Join(outDir, "EFI/boot/grubx64.efi"), testutil.FileEquals, "grubx64.efi from gadget") 3099 c.Check(filepath.Join(outDir, "EFI/ubuntu/grub.cfg"), testutil.FileEquals, 3100 `# Snapd-Boot-Config-Edition: 1 3101 managed grub.cfg from disk`) 3102 c.Check(filepath.Join(outDir, "foo"), testutil.FileEquals, "foo from disk") 3103 case "rollback": 3104 c.Check(filepath.Join(outDir, "EFI/boot/grubx64.efi"), testutil.FileEquals, "grubx64.efi from disk") 3105 c.Check(filepath.Join(outDir, "EFI/ubuntu/grub.cfg"), testutil.FileEquals, existingGrubCfg) 3106 c.Check(filepath.Join(outDir, "foo"), testutil.FileEquals, "foo from disk") 3107 default: 3108 c.Fatalf("unexpected step: %q", step.name) 3109 } 3110 verifyDirContents(c, filepath.Join(s.backup, "struct-0"), expectedFileStamps) 3111 } 3112 } 3113 3114 var ( 3115 // based on pc gadget 3116 psForObserver = &gadget.LaidOutStructure{ 3117 VolumeStructure: &gadget.VolumeStructure{ 3118 Size: 2048, 3119 Role: gadget.SystemBoot, 3120 Filesystem: "ext4", 3121 Content: []gadget.VolumeContent{ 3122 {Source: "foo", Target: "foo"}, 3123 }, 3124 Update: gadget.VolumeUpdate{ 3125 Edition: 1, 3126 }, 3127 }, 3128 } 3129 ) 3130 3131 func (s *mountedfilesystemTestSuite) TestMountedUpdaterObserverPreserveNewFile(c *C) { 3132 gd := []gadgetData{ 3133 {name: "foo", content: "foo from gadget"}, 3134 } 3135 makeGadgetData(c, s.dir, gd) 3136 3137 outDir := filepath.Join(c.MkDir(), "out-dir") 3138 3139 obs := &mockContentUpdateObserver{ 3140 c: c, 3141 expectedStruct: psForObserver, 3142 preserveTargets: []string{"foo"}, 3143 } 3144 rw, err := gadget.NewMountedFilesystemUpdater(s.dir, psForObserver, s.backup, 3145 func(to *gadget.LaidOutStructure) (string, error) { 3146 c.Check(to, DeepEquals, psForObserver) 3147 return outDir, nil 3148 }, 3149 obs) 3150 c.Assert(err, IsNil) 3151 c.Assert(rw, NotNil) 3152 3153 expectedNewFileChanges := map[string][]*mockContentChange{ 3154 outDir: { 3155 {"foo", &gadget.ContentChange{After: filepath.Join(s.dir, "foo")}}, 3156 }, 3157 } 3158 expectedStamps := map[string]contentType{ 3159 "foo.ignore": typeFile, 3160 } 3161 // file does not exist 3162 err = rw.Backup() 3163 c.Assert(err, IsNil) 3164 // no stamps 3165 verifyDirContents(c, filepath.Join(s.backup, "struct-0"), expectedStamps) 3166 // observer got notified about change 3167 c.Assert(obs.contentUpdate, DeepEquals, expectedNewFileChanges) 3168 3169 obs.reset() 3170 3171 // try the same pass again 3172 err = rw.Backup() 3173 c.Assert(err, IsNil) 3174 verifyDirContents(c, filepath.Join(s.backup, "struct-0"), expectedStamps) 3175 // observer already requested the change to be ignored once 3176 c.Assert(obs.contentUpdate, HasLen, 0) 3177 3178 // file does not exist and is not written 3179 err = rw.Update() 3180 c.Assert(err, Equals, gadget.ErrNoUpdate) 3181 c.Assert(filepath.Join(outDir, "foo"), testutil.FileAbsent) 3182 3183 // nothing happens on rollback 3184 err = rw.Rollback() 3185 c.Assert(err, IsNil) 3186 c.Assert(filepath.Join(outDir, "foo"), testutil.FileAbsent) 3187 } 3188 3189 func (s *mountedfilesystemTestSuite) TestMountedUpdaterObserverPreserveExistingFile(c *C) { 3190 gd := []gadgetData{ 3191 {name: "foo", content: "foo from gadget"}, 3192 } 3193 makeGadgetData(c, s.dir, gd) 3194 3195 outDir := filepath.Join(c.MkDir(), "out-dir") 3196 3197 obs := &mockContentUpdateObserver{ 3198 c: c, 3199 expectedStruct: psForObserver, 3200 preserveTargets: []string{"foo"}, 3201 } 3202 rw, err := gadget.NewMountedFilesystemUpdater(s.dir, psForObserver, s.backup, 3203 func(to *gadget.LaidOutStructure) (string, error) { 3204 c.Check(to, DeepEquals, psForObserver) 3205 return outDir, nil 3206 }, 3207 obs) 3208 c.Assert(err, IsNil) 3209 c.Assert(rw, NotNil) 3210 3211 // file exists now 3212 makeExistingData(c, outDir, []gadgetData{ 3213 {target: "foo", content: "foo from disk"}, 3214 }) 3215 expectedExistingFileChanges := map[string][]*mockContentChange{ 3216 outDir: { 3217 {"foo", &gadget.ContentChange{ 3218 After: filepath.Join(s.dir, "foo"), 3219 Before: filepath.Join(s.backup, "struct-0/foo.backup"), 3220 }}, 3221 }, 3222 } 3223 expectedExistingFileStamps := map[string]contentType{ 3224 "foo.ignore": typeFile, 3225 } 3226 err = rw.Backup() 3227 c.Assert(err, IsNil) 3228 verifyDirContents(c, filepath.Join(s.backup, "struct-0"), expectedExistingFileStamps) 3229 // get notified about change 3230 c.Assert(obs.contentUpdate, DeepEquals, expectedExistingFileChanges) 3231 3232 obs.reset() 3233 // backup called again (eg. after reset) 3234 err = rw.Backup() 3235 c.Assert(err, IsNil) 3236 verifyDirContents(c, filepath.Join(s.backup, "struct-0"), expectedExistingFileStamps) 3237 // observer already requested the change to be ignored once 3238 c.Assert(obs.contentUpdate, HasLen, 0) 3239 3240 // and nothing gets updated 3241 err = rw.Update() 3242 c.Assert(err, Equals, gadget.ErrNoUpdate) 3243 c.Check(filepath.Join(outDir, "foo"), testutil.FileEquals, "foo from disk") 3244 3245 // the file existed and was preserved, nothing gets removed on rollback 3246 err = rw.Rollback() 3247 c.Assert(err, IsNil) 3248 c.Check(filepath.Join(outDir, "foo"), testutil.FileEquals, "foo from disk") 3249 }