github.com/cvmfs/docker-graphdriver@v0.0.0-20181206110523-155ec6df0521/plugins/aufs_cvmfs/aufs/aufs.go (about) 1 // +build linux 2 3 /* 4 5 aufs driver directory structure 6 7 . 8 ├── layers // Metadata of layers 9 │ ├── 1 10 │ ├── 2 11 │ └── 3 12 ├── diff // Content of the layer 13 │ ├── 1 // Contains layers that need to be mounted for the id 14 │ ├── 2 15 │ └── 3 16 └── mnt // Mount points for the rw layers to be mounted 17 ├── 1 18 ├── 2 19 └── 3 20 21 */ 22 23 package aufs 24 25 import ( 26 "bufio" 27 "fmt" 28 "io" 29 "io/ioutil" 30 "os" 31 "os/exec" 32 "path" 33 "path/filepath" 34 "strings" 35 "sync" 36 "syscall" 37 "time" 38 39 "github.com/Sirupsen/logrus" 40 "github.com/vbatts/tar-split/tar/storage" 41 42 "github.com/docker/docker/daemon/graphdriver" 43 "github.com/docker/docker/pkg/archive" 44 "github.com/docker/docker/pkg/chrootarchive" 45 "github.com/docker/docker/pkg/directory" 46 "github.com/docker/docker/pkg/idtools" 47 "github.com/docker/docker/pkg/locker" 48 mountpk "github.com/docker/docker/pkg/mount" 49 50 "github.com/cvmfs/docker-graphdriver/plugins/util" 51 "github.com/opencontainers/selinux/go-selinux/label" 52 ) 53 54 var ( 55 // ErrAufsNotSupported is returned if aufs is not supported by the host. 56 ErrAufsNotSupported = fmt.Errorf("AUFS was not found in /proc/filesystems") 57 // ErrAufsNested means aufs cannot be used bc we are in a user namespace 58 ErrAufsNested = fmt.Errorf("AUFS cannot be used in non-init user namespace") 59 backingFs = "<unknown>" 60 61 enableDirpermLock sync.Once 62 enableDirperm bool 63 ) 64 65 func init() { 66 graphdriver.Register("aufs", Init) 67 } 68 69 // Driver contains information about the filesystem mounted. 70 type Driver struct { 71 sync.Mutex 72 root string 73 uidMaps []idtools.IDMap 74 gidMaps []idtools.IDMap 75 ctr *graphdriver.RefCounter 76 pathCacheLock sync.Mutex 77 pathCache map[string]string 78 naiveDiff graphdriver.DiffDriver 79 locker *locker.Locker 80 cvmfsManager util.ICvmfsManager 81 82 cvmfsMountMethod string 83 cvmfsMountPath string 84 } 85 86 // Init returns a new AUFS driver. 87 // An error is returned if AUFS is not supported. 88 func Init(root string, options []string, uidMaps, gidMaps []idtools.IDMap) (graphdriver.Driver, error) { 89 os.MkdirAll(root, os.ModePerm) 90 os.MkdirAll("/dev/shm", os.ModePerm) 91 exec.Command("mount", "-t", "tmpfs", "shmfs", "/dev/shm").Run() 92 93 fsMagic, err := graphdriver.GetFSMagic(root) 94 if err != nil { 95 return nil, err 96 } 97 if fsName, ok := graphdriver.FsNames[fsMagic]; ok { 98 backingFs = fsName 99 } 100 101 switch fsMagic { 102 case graphdriver.FsMagicAufs, graphdriver.FsMagicBtrfs, graphdriver.FsMagicEcryptfs: 103 logrus.Errorf("AUFS is not supported over %s", backingFs) 104 return nil, graphdriver.ErrIncompatibleFS 105 } 106 107 paths := []string{ 108 "mnt", 109 "diff", 110 "layers", 111 } 112 113 a := &Driver{ 114 root: root, 115 uidMaps: uidMaps, 116 gidMaps: gidMaps, 117 pathCache: make(map[string]string), 118 ctr: graphdriver.NewRefCounter(graphdriver.NewFsChecker(graphdriver.FsMagicAufs)), 119 cvmfsMountPath: "/mnt/cvmfs", 120 } 121 122 if err := a.configureCvmfs(options); err != nil { 123 return nil, err 124 } 125 126 a.cvmfsManager = util.NewCvmfsManager(a.cvmfsMountPath, a.cvmfsMountMethod) 127 128 rootUID, rootGID, err := idtools.GetRootUIDGID(uidMaps, gidMaps) 129 if err != nil { 130 return nil, err 131 } 132 // Create the root aufs driver dir and return 133 // if it already exists 134 // If not populate the dir structure 135 if err := idtools.MkdirAllAs(root, 0700, rootUID, rootGID); err != nil { 136 if os.IsExist(err) { 137 return a, nil 138 } 139 return nil, err 140 } 141 142 // if err := mountpk.MakePrivate(root); err != nil { 143 // return nil, err 144 // } 145 146 // Populate the dir structure 147 for _, p := range paths { 148 if err := idtools.MkdirAllAs(path.Join(root, p), 0700, rootUID, rootGID); err != nil { 149 return nil, err 150 } 151 } 152 153 a.naiveDiff = graphdriver.NewNaiveDiffDriver(a, uidMaps, gidMaps) 154 return a, nil 155 } 156 157 func (a *Driver) rootPath() string { 158 return a.root 159 } 160 161 func (*Driver) String() string { 162 return "aufs" 163 } 164 165 // Status returns current information about the filesystem such as root directory, number of directories mounted, etc. 166 func (a *Driver) Status() [][2]string { 167 ids, _ := loadIds(path.Join(a.rootPath(), "layers")) 168 return [][2]string{ 169 {"Root Dir", a.rootPath()}, 170 {"Backing Filesystem", backingFs}, 171 {"Dirs", fmt.Sprintf("%d", len(ids))}, 172 {"Dirperm1 Supported", fmt.Sprintf("%v", useDirperm())}, 173 } 174 } 175 176 // GetMetadata not implemented 177 func (a *Driver) GetMetadata(id string) (map[string]string, error) { 178 return nil, nil 179 } 180 181 // Exists returns true if the given id is registered with 182 // this driver 183 func (a *Driver) Exists(id string) bool { 184 if _, err := os.Lstat(path.Join(a.rootPath(), "layers", id)); err != nil { 185 return false 186 } 187 return true 188 } 189 190 // CreateReadWrite creates a layer that is writable for use as a container 191 // file system. 192 func (a *Driver) CreateReadWrite(id, parent string, opts *graphdriver.CreateOpts) error { 193 fmt.Printf("CreateReadWrite(%s, %s)\n", id, parent) 194 195 return a.Create(id, parent, opts) 196 } 197 198 // Create three folders for each id 199 // mnt, layers, and diff 200 func (a *Driver) Create(id, parent string, opts *graphdriver.CreateOpts) error { 201 fmt.Printf("Create(%s, %s)\n", id, parent) 202 203 if opts != nil && len(opts.StorageOpt) != 0 { 204 return fmt.Errorf("--storage-opt is not supported for aufs") 205 } 206 207 if err := a.createDirsFor(id); err != nil { 208 return err 209 } 210 // Write the layers metadata 211 f, err := os.Create(path.Join(a.rootPath(), "layers", id)) 212 if err != nil { 213 return err 214 } 215 defer f.Close() 216 217 if parent != "" { 218 ids, err := getParentIDs(a.rootPath(), parent) 219 if err != nil { 220 return err 221 } 222 223 if _, err := fmt.Fprintln(f, parent); err != nil { 224 return err 225 } 226 for _, i := range ids { 227 if _, err := fmt.Fprintln(f, i); err != nil { 228 return err 229 } 230 } 231 } 232 233 return nil 234 } 235 236 // createDirsFor creates two directories for the given id. 237 // mnt and diff 238 func (a *Driver) createDirsFor(id string) error { 239 paths := []string{ 240 "mnt", 241 "diff", 242 } 243 244 rootUID, rootGID, err := idtools.GetRootUIDGID(a.uidMaps, a.gidMaps) 245 if err != nil { 246 return err 247 } 248 // Directory permission is 0755. 249 // The path of directories are <aufs_root_path>/mnt/<image_id> 250 // and <aufs_root_path>/diff/<image_id> 251 for _, p := range paths { 252 if err := idtools.MkdirAllAs(path.Join(a.rootPath(), p, id), 0755, rootUID, rootGID); err != nil { 253 return err 254 } 255 } 256 return nil 257 } 258 259 // Helper function to debug EBUSY errors on remove. 260 func debugEBusy(mountPath string) (out []string, err error) { 261 // lsof is not part of GNU coreutils. This is a best effort 262 // attempt to detect offending processes. 263 c := exec.Command("lsof") 264 265 r, err := c.StdoutPipe() 266 if err != nil { 267 return nil, fmt.Errorf("Assigning pipes failed with %v", err) 268 } 269 270 if err := c.Start(); err != nil { 271 return nil, fmt.Errorf("Starting %s failed with %v", c.Path, err) 272 } 273 274 defer func() { 275 waiterr := c.Wait() 276 if waiterr != nil && err == nil { 277 err = fmt.Errorf("Waiting for %s failed with %v", c.Path, waiterr) 278 } 279 }() 280 281 sc := bufio.NewScanner(r) 282 for sc.Scan() { 283 entry := sc.Text() 284 if strings.Contains(entry, mountPath) { 285 out = append(out, entry, "\n") 286 } 287 } 288 289 return out, nil 290 } 291 292 // Remove will unmount and remove the given id. 293 func (a *Driver) Remove(id string) error { 294 fmt.Printf("Remove(%s)\n", id) 295 296 a.pathCacheLock.Lock() 297 mountpoint, exists := a.pathCache[id] 298 a.pathCacheLock.Unlock() 299 if !exists { 300 mountpoint = a.getMountpoint(id) 301 } 302 303 var retries int 304 for { 305 mounted, err := a.mounted(mountpoint) 306 if err != nil { 307 return err 308 } 309 if !mounted { 310 break 311 } 312 313 if err := a.unmount(mountpoint); err != nil { 314 if err != syscall.EBUSY { 315 return fmt.Errorf("aufs: unmount error: %s: %v", mountpoint, err) 316 } 317 if retries >= 5 { 318 out, debugErr := debugEBusy(mountpoint) 319 if debugErr == nil { 320 logrus.Warnf("debugEBusy returned %v", out) 321 } 322 return fmt.Errorf("aufs: unmount error after retries: %s: %v", mountpoint, err) 323 } 324 // If unmount returns EBUSY, it could be a transient error. Sleep and retry. 325 retries++ 326 logrus.Warnf("unmount failed due to EBUSY: retry count: %d", retries) 327 time.Sleep(100 * time.Millisecond) 328 continue 329 } 330 break 331 } 332 333 // Atomically remove each directory in turn by first moving it out of the 334 // way (so that docker doesn't find it anymore) before doing removal of 335 // the whole tree. 336 tmpMntPath := path.Join(a.mntPath(), fmt.Sprintf("%s-removing", id)) 337 if err := os.Rename(mountpoint, tmpMntPath); err != nil && !os.IsNotExist(err) { 338 if err == syscall.EBUSY { 339 logrus.Warn("os.Rename err due to EBUSY") 340 out, debugErr := debugEBusy(mountpoint) 341 if debugErr == nil { 342 logrus.Warnf("debugEBusy returned %v", out) 343 } 344 } 345 return err 346 } 347 defer os.RemoveAll(tmpMntPath) 348 349 tmpDiffpath := path.Join(a.diffPath(), fmt.Sprintf("%s-removing", id)) 350 if err := os.Rename(a.getDiffPath(id), tmpDiffpath); err != nil && !os.IsNotExist(err) { 351 return err 352 } 353 defer os.RemoveAll(tmpDiffpath) 354 355 // Remove the layers file for the id 356 if err := os.Remove(path.Join(a.rootPath(), "layers", id)); err != nil && !os.IsNotExist(err) { 357 return err 358 } 359 360 a.pathCacheLock.Lock() 361 delete(a.pathCache, id) 362 a.pathCacheLock.Unlock() 363 return nil 364 } 365 366 // Get returns the rootfs path for the id. 367 // This will mount the dir at its given path 368 func (a *Driver) Get(id, mountLabel string) (string, error) { 369 fmt.Printf("Get(%s, %s)\n", id, mountLabel) 370 371 parents, err := a.getParentLayerPaths(id) 372 if err != nil { 373 return "", err 374 } 375 376 a.pathCacheLock.Lock() 377 m, exists := a.pathCache[id] 378 a.pathCacheLock.Unlock() 379 380 if !exists { 381 m = a.getDiffPath(id) 382 if len(parents) > 0 { 383 m = a.getMountpoint(id) 384 } 385 } 386 if count := a.ctr.Increment(m); count > 1 { 387 return m, nil 388 } 389 390 // If a dir does not have a parent ( no layers )do not try to mount 391 // just return the diff path to the data 392 if len(parents) > 0 { 393 if err := a.mount(id, m, mountLabel, parents); err != nil { 394 return "", err 395 } 396 } 397 398 a.pathCacheLock.Lock() 399 a.pathCache[id] = m 400 a.pathCacheLock.Unlock() 401 return m, nil 402 } 403 404 // Put unmounts and updates list of active mounts. 405 func (a *Driver) Put(id string) error { 406 fmt.Printf("Put(%s)\n", id) 407 408 a.pathCacheLock.Lock() 409 m, exists := a.pathCache[id] 410 if !exists { 411 m = a.getMountpoint(id) 412 a.pathCache[id] = m 413 } 414 a.pathCacheLock.Unlock() 415 if count := a.ctr.Decrement(m); count > 0 { 416 return nil 417 } 418 419 err := a.unmount(m) 420 if err != nil { 421 logrus.Debugf("Failed to unmount %s aufs: %v", id, err) 422 } 423 return err 424 } 425 426 // isParent returns if the passed in parent is the direct parent of the passed in layer 427 func (a *Driver) isParent(id, parent string) bool { 428 fmt.Printf("isParent(%s, %s)\n", id, parent) 429 430 parents, _ := getParentIDs(a.rootPath(), id) 431 if parent == "" && len(parents) > 0 { 432 return false 433 } 434 return !(len(parents) > 0 && parent != parents[0]) 435 } 436 437 // Diff produces an archive of the changes between the specified 438 // layer and its parent layer which may be "". 439 func (a *Driver) Diff(id, parent string) (io.ReadCloser, error) { 440 fmt.Printf("Diff(%s, %s)\n", id, parent) 441 var newThinLayer string 442 var exportPath string 443 var isThin bool 444 445 thin, err := a.getParentThinLayer(id) 446 447 if err == nil { 448 fmt.Println("Found a thin image!") 449 isThin = true 450 451 orig := a.getDiffPath(id) 452 453 fmt.Printf("Orig diffpath: %s\n", orig) 454 newLayer, err := a.cvmfsManager.UploadNewLayer(orig) 455 if err != nil { 456 fmt.Printf("error on UploadNewLayer(): %s", err.Error()) 457 return nil, err 458 } 459 460 fmt.Printf("Uploaded hash is: %s\n", newLayer.Digest) 461 462 thin.AddLayer(newLayer) 463 if newThinLayer, err = util.WriteThinFile(thin); err != nil { 464 fmt.Printf("Failed to create thin file") 465 return nil, err 466 } 467 } else { 468 fmt.Println("Didn't find a thin image parent!") 469 fmt.Println(err) 470 } 471 472 if isThin { 473 exportPath = newThinLayer 474 } else { 475 exportPath = path.Join(a.rootPath(), "diff", id) 476 } 477 478 if !a.isParent(id, parent) { 479 return a.naiveDiff.Diff(id, parent) 480 } 481 482 return archive.TarWithOptions(exportPath, &archive.TarOptions{ 483 Compression: archive.Uncompressed, 484 ExcludePatterns: []string{archive.WhiteoutMetaPrefix + "*", "!" + archive.WhiteoutOpaqueDir}, 485 UIDMaps: a.uidMaps, 486 GIDMaps: a.gidMaps, 487 }) 488 } 489 490 type fileGetNilCloser struct { 491 storage.FileGetter 492 } 493 494 func (f fileGetNilCloser) Close() error { 495 return nil 496 } 497 498 // DiffGetter returns a FileGetCloser that can read files from the directory that 499 // contains files for the layer differences. Used for direct access for tar-split. 500 func (a *Driver) DiffGetter(id string) (graphdriver.FileGetCloser, error) { 501 fmt.Printf("DiffGetter(%s)\n", id) 502 503 p := path.Join(a.rootPath(), "diff", id) 504 return fileGetNilCloser{storage.NewPathFileGetter(p)}, nil 505 } 506 507 func (a *Driver) applyDiff(id string, diff io.Reader) error { 508 fmt.Printf("applyDiff(%s)\n", id) 509 510 return chrootarchive.UntarUncompressed(diff, path.Join(a.rootPath(), "diff", id), &archive.TarOptions{ 511 UIDMaps: a.uidMaps, 512 GIDMaps: a.gidMaps, 513 }) 514 } 515 516 // DiffSize calculates the changes between the specified id 517 // and its parent and returns the size in bytes of the changes 518 // relative to its base filesystem directory. 519 func (a *Driver) DiffSize(id, parent string) (size int64, err error) { 520 fmt.Printf("DiffSize(%s, %s)\n", id, parent) 521 522 if !a.isParent(id, parent) { 523 return a.naiveDiff.DiffSize(id, parent) 524 } 525 // AUFS doesn't need the parent layer to calculate the diff size. 526 return directory.Size(path.Join(a.rootPath(), "diff", id)) 527 } 528 529 // ApplyDiff extracts the changeset from the given diff into the 530 // layer with the specified id and parent, returning the size of the 531 // new layer in bytes. 532 func (a *Driver) ApplyDiff(id, parent string, diff io.Reader) (size int64, err error) { 533 fmt.Printf("ApplyDiff(%s, %s)\n", id, parent) 534 535 if !a.isParent(id, parent) { 536 return a.naiveDiff.ApplyDiff(id, parent, diff) 537 } 538 539 // AUFS doesn't need the parent id to apply the diff if it is the direct parent. 540 if err = a.applyDiff(id, diff); err != nil { 541 return 542 } 543 544 return a.DiffSize(id, parent) 545 } 546 547 // Changes produces a list of changes between the specified layer 548 // and its parent layer. If parent is "", then all changes will be ADD changes. 549 func (a *Driver) Changes(id, parent string) ([]archive.Change, error) { 550 fmt.Printf("Changes(%s, %s)\n", id, parent) 551 552 if !a.isParent(id, parent) { 553 return a.naiveDiff.Changes(id, parent) 554 } 555 556 // AUFS doesn't have snapshots, so we need to get changes from all parent 557 // layers. 558 layers, err := a.getParentLayerPaths(id) 559 if err != nil { 560 return nil, err 561 } 562 return archive.Changes(layers, path.Join(a.rootPath(), "diff", id)) 563 } 564 565 func (a *Driver) getParentLayerPaths(id string) ([]string, error) { 566 fmt.Printf("getParentLayerPaths(%s)\n", id) 567 568 parentIds, err := getParentIDs(a.rootPath(), id) 569 if err != nil { 570 return nil, err 571 } 572 layers := make([]string, len(parentIds)) 573 foundThin := false 574 ctr := 0 575 576 // Get the diff paths for all the parent ids 577 for _, p := range parentIds { 578 diffPath := a.getDiffPath(p) 579 580 if util.IsThinImageLayer(diffPath) && (foundThin == false) { 581 nested_layers := util.GetNestedLayerIDs(diffPath) 582 583 var err error 584 if a.cvmfsMountMethod == "internal" { 585 err = a.cvmfsManager.GetLayers(nested_layers...) 586 } 587 588 if err != nil { 589 return nil, err 590 } 591 592 cvmfs_paths := util.GetCvmfsLayerPaths(nested_layers, a.cvmfsMountPath) 593 layers = util.ExpandCvmfsLayerPaths(layers, cvmfs_paths, ctr) 594 foundThin = true 595 ctr += len(cvmfs_paths) 596 } else if !util.IsThinImageLayer(diffPath) { 597 layers[ctr] = path.Join(a.rootPath(), "diff", p) 598 ctr += 1 599 } 600 } 601 602 layers = layers[:ctr] 603 return layers, nil 604 } 605 606 func (a *Driver) mount(id string, target string, mountLabel string, layers []string) error { 607 a.Lock() 608 defer a.Unlock() 609 610 // If the id is mounted or we get an error return 611 if mounted, err := a.mounted(target); err != nil || mounted { 612 return err 613 } 614 615 rw := a.getDiffPath(id) 616 617 if err := a.aufsMount(layers, rw, target, mountLabel); err != nil { 618 return fmt.Errorf("error creating aufs mount to %s: %v", target, err) 619 } 620 return nil 621 } 622 623 func (a *Driver) unmount(mountPath string) error { 624 a.Lock() 625 defer a.Unlock() 626 627 if mounted, err := a.mounted(mountPath); err != nil || !mounted { 628 return err 629 } 630 if err := Unmount(mountPath); err != nil { 631 return err 632 } 633 return nil 634 } 635 636 func (a *Driver) mounted(mountpoint string) (bool, error) { 637 return graphdriver.Mounted(graphdriver.FsMagicAufs, mountpoint) 638 } 639 640 // Cleanup aufs and unmount all mountpoints 641 func (a *Driver) Cleanup() error { 642 if a.cvmfsMountMethod == "internal" { 643 a.cvmfsManager.PutAll() 644 } 645 646 exec.Command("umount", "/dev/shm").Run() 647 648 var dirs []string 649 if err := filepath.Walk(a.mntPath(), func(path string, info os.FileInfo, err error) error { 650 if err != nil { 651 return err 652 } 653 if !info.IsDir() { 654 return nil 655 } 656 dirs = append(dirs, path) 657 return nil 658 }); err != nil { 659 return err 660 } 661 662 for _, m := range dirs { 663 if err := a.unmount(m); err != nil { 664 logrus.Debugf("aufs error unmounting %s: %s", m, err) 665 } 666 } 667 return mountpk.Unmount(a.root) 668 } 669 670 func (a *Driver) aufsMount(ro []string, rw, target, mountLabel string) (err error) { 671 defer func() { 672 if err != nil { 673 Unmount(target) 674 } 675 }() 676 677 // Mount options are clipped to page size(4096 bytes). If there are more 678 // layers then these are remounted individually using append. 679 680 offset := 54 681 if useDirperm() { 682 offset += len("dirperm1") 683 } 684 b := make([]byte, syscall.Getpagesize()-len(mountLabel)-offset) // room for xino & mountLabel 685 bp := copy(b, fmt.Sprintf("br:%s=rw", rw)) 686 687 index := 0 688 for ; index < len(ro); index++ { 689 layer := fmt.Sprintf(":%s=ro+wh", ro[index]) 690 if bp+len(layer) > len(b) { 691 break 692 } 693 bp += copy(b[bp:], layer) 694 } 695 696 opts := "dio,xino=/dev/shm/aufs.xino" 697 if useDirperm() { 698 opts += ",dirperm1" 699 } 700 data := label.FormatMountLabel(fmt.Sprintf("%s,%s", string(b[:bp]), opts), mountLabel) 701 if err = mount("none", target, "aufs", 0, data); err != nil { 702 return 703 } 704 705 for ; index < len(ro); index++ { 706 layer := fmt.Sprintf(":%s=ro+wh", ro[index]) 707 data := label.FormatMountLabel(fmt.Sprintf("append%s", layer), mountLabel) 708 if err = mount("none", target, "aufs", syscall.MS_REMOUNT, data); err != nil { 709 return 710 } 711 } 712 713 return 714 } 715 716 // useDirperm checks dirperm1 mount option can be used with the current 717 // version of aufs. 718 func useDirperm() bool { 719 enableDirpermLock.Do(func() { 720 base, err := ioutil.TempDir("", "docker-aufs-base") 721 if err != nil { 722 logrus.Errorf("error checking dirperm1: %v", err) 723 return 724 } 725 defer os.RemoveAll(base) 726 727 union, err := ioutil.TempDir("", "docker-aufs-union") 728 if err != nil { 729 logrus.Errorf("error checking dirperm1: %v", err) 730 return 731 } 732 defer os.RemoveAll(union) 733 734 opts := fmt.Sprintf("br:%s,dirperm1,xino=/dev/shm/aufs.xino", base) 735 if err := mount("none", union, "aufs", 0, opts); err != nil { 736 return 737 } 738 enableDirperm = true 739 if err := Unmount(union); err != nil { 740 logrus.Errorf("error checking dirperm1: failed to unmount %v", err) 741 } 742 }) 743 return enableDirperm 744 } 745 746 func (a *Driver) configureCvmfs(options []string) error { 747 m, err := util.ParseOptions(options) 748 749 if err != nil { 750 return err 751 } 752 753 if method, ok := m["cvmfsMountMethod"]; !ok { 754 a.cvmfsMountMethod = "internal" 755 } else { 756 a.cvmfsMountMethod = method 757 } 758 759 a.cvmfsMountPath = path.Join(a.root, "cvmfs") 760 os.MkdirAll(a.cvmfsMountPath, os.ModePerm) 761 762 return nil 763 }