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