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