diff --git a/eden/fs/cli/config.py b/eden/fs/cli/config.py index 837f1b0a2e..adf83fd030 100644 --- a/eden/fs/cli/config.py +++ b/eden/fs/cli/config.py @@ -22,7 +22,19 @@ import time import typing import uuid from pathlib import Path -from typing import Any, Dict, IO, KeysView, List, Mapping, Optional, Set, Tuple, Union +from typing import ( + Any, + Callable, + Dict, + IO, + KeysView, + List, + Mapping, + Optional, + Set, + Tuple, + Union, +) import facebook.eden.ttypes as eden_ttypes import toml @@ -32,7 +44,15 @@ from facebook.eden.ttypes import MountInfo as ThriftMountInfo, MountState from filelock import BaseFileLock, FileLock from . import configinterpolator, configutil, telemetry, util, version -from .util import HealthStatus, print_stderr, Spinner, write_file_atomically +from .util import ( + FUSE_MOUNT_PROTOCOL_STRING, + HealthStatus, + NFS_MOUNT_PROTOCOL_STRING, + print_stderr, + PRJFS_MOUNT_PROTOCOL_STRING, + Spinner, + write_file_atomically, +) try: from eden.thrift import client # @manual @@ -85,7 +105,11 @@ DEFAULT_REVISION = { # supported repo name -> default bookmark SUPPORTED_REPOS: KeysView[str] = DEFAULT_REVISION.keys() -SUPPORTED_MOUNT_PROTOCOLS = {"fuse", "nfs", "prjfs"} +SUPPORTED_MOUNT_PROTOCOLS: Set[str] = { + FUSE_MOUNT_PROTOCOL_STRING, + NFS_MOUNT_PROTOCOL_STRING, + PRJFS_MOUNT_PROTOCOL_STRING, +} # Create a readme file with this name in the mount point directory. # The intention is for this to contain instructions telling users what to do if their @@ -1558,6 +1582,57 @@ class EdenCheckout: self.save_config(new_config) +# Fuse is still not functional on Ventura, so users will need to use NFS on +# Ventura. +def should_migrate_mount_protocol_to_nfs(instance: EdenInstance) -> bool: + if sys.platform != "darwin": + return False + + if util.is_sandcastle(): + return False + + ventura_os_version = "22.0.0" + + if tuple(os.uname().release.split(".")) >= tuple(ventura_os_version.split(".")): + return instance.get_config_bool("core.migrate_existing_to_nfs", default=False) + + return False + + +# Checks for any non NFS mounts and migrates them to NFS. +def _do_nfs_migration( + instance: EdenInstance, get_migration_success_message: Callable[[str], str] +) -> None: + migrate_mounts = False + for checkout in instance.get_checkouts(): + if checkout.get_config().mount_protocol != util.NFS_MOUNT_PROTOCOL_STRING: + migrate_mounts = True + if not migrate_mounts: + # most the time this should be the case. we only need to migrate mounts + # once, and then we should just be able to skip this all other times. + return + + print("migrating mounts to NFS ...") + + for checkout in instance.get_checkouts(): + if checkout.get_config().mount_protocol != util.NFS_MOUNT_PROTOCOL_STRING: + checkout.migrate_mount_protocol(util.NFS_MOUNT_PROTOCOL_STRING) + + instance.log_sample("migrate_existing_clones_to_nfs") + print(get_migration_success_message(util.NFS_MOUNT_PROTOCOL_STRING)) + + +def _do_manual_migration( + instance: EdenInstance, + migrate_to: str, + get_migration_success_message: Callable[[str], str], +) -> None: + for checkout in instance.get_checkouts(): + checkout.migrate_mount_protocol(migrate_to) + + print(get_migration_success_message(migrate_to)) + + def detect_nested_checkout( path: Union[str, Path], instance: EdenInstance, diff --git a/eden/fs/cli/main.py b/eden/fs/cli/main.py index c9bc98739f..4585b4884e 100644 --- a/eden/fs/cli/main.py +++ b/eden/fs/cli/main.py @@ -1699,6 +1699,8 @@ class StartCmd(Subcmd): instance, daemon_binary, args.edenfs_args ) + if config_mod.should_migrate_mount_protocol_to_nfs(instance): + config_mod._do_nfs_migration(instance, get_migration_success_message) return daemon.start_edenfs_service(instance, daemon_binary, args.edenfs_args) def start_in_foreground( @@ -2029,7 +2031,11 @@ re-open these files after EdenFS is restarted. self._do_stop(instance, old_pid, timeout=15) if migrate_to is not None: - self._do_migration(instance, migrate_to) + config_mod._do_manual_migration( + instance, migrate_to, get_migration_success_message + ) + elif config_mod.should_migrate_mount_protocol_to_nfs(instance): + config_mod._do_nfs_migration(instance, get_migration_success_message) return self._finish_restart(instance) def _force_restart( @@ -2068,12 +2074,6 @@ re-open these files after EdenFS is restarted. os.kill(pid, signal.SIGTERM) self._wait_for_stop(instance, pid, timeout) - def _do_migration(self, instance: EdenInstance, migrate_to: str) -> None: - for checkout in instance.get_checkouts(): - checkout.migrate_mount_protocol(migrate_to) - - print(get_migration_success_message(migrate_to)) - def _finish_restart(self, instance: EdenInstance) -> int: exit_code = daemon.start_edenfs_service( instance, daemon_binary=self.args.daemon_binary diff --git a/eden/fs/cli/test/config_test.py b/eden/fs/cli/test/config_test.py index 32d347503a..906a2403a1 100644 --- a/eden/fs/cli/test/config_test.py +++ b/eden/fs/cli/test/config_test.py @@ -12,14 +12,15 @@ import os import sys import unittest from pathlib import Path +from typing import Dict import toml import toml.decoder +from eden.fs.cli.config import EdenInstance from eden.test_support.temporary_directory import TemporaryDirectoryMixin from eden.test_support.testcase import EdenTestCaseBase from .. import config as config_mod, configutil, util -from ..config import EdenInstance from ..configinterpolator import EdenConfigInterpolator from ..configutil import EdenConfigParser, UnexpectedType @@ -543,3 +544,98 @@ class EdenInstanceConstructionTest(unittest.TestCase): ) self.assertEqual(instance.etc_eden_dir, Path("/etc/eden")) self.assertEqual(instance.home_dir, Path("/home/testuser/")) + + +class NFSMigrationTest(EdenTestCaseBase): + def setUp(self) -> None: + super().setUp() + self._user = "bob" + self._state_dir = self.tmp_dir / ".eden" + self._etc_eden_dir = self.tmp_dir / "etc/eden" + self._config_d = self.tmp_dir / "etc/eden/config.d" + self._home_dir = self.tmp_dir / "home" / self._user + self._interpolate_dict = { + "USER": self._user, + "USER_ID": "42", + "HOME": str(self._home_dir), + } + + self._state_dir.mkdir() + self._config_d.mkdir(exist_ok=True, parents=True) + self._home_dir.mkdir(exist_ok=True, parents=True) + + def setup_config_files(self, mounts: Dict[str, str]) -> None: + config_json_list = ",\n".join( + [f'"{self.tmp_dir}/{mount}" : "{mount}"' for mount in mounts] + ) + config_json = f"""{{ +{config_json_list} +}}""" + (self._state_dir / "config.json").write_text(config_json) + + (self._state_dir / "clients").mkdir() + for mount, initial_mount_protocol in mounts.items(): + (self._state_dir / "clients" / mount).mkdir() + (self._state_dir / "clients" / mount / "config.toml").write_text( + f""" +[repository] +path = "{self.tmp_dir}/.eden-backing-repos/test" +type = "hg" +protocol = "{initial_mount_protocol}" +""" + ) + + def check_migrate_nfs(self, mounts: Dict[str, str]) -> None: + self.setup_config_files(mounts) + + cmdline = [ + b"/usr/local/libexec/eden/edenfs", + b"--edenfs", + b"--edenDir", + str(self._state_dir).encode("utf-8"), + b"--etcEdenDir", + str(self._config_d).encode("utf-8"), + b"--configPath", + str(self._home_dir).encode("utf-8"), + b"--edenfsctlPath", + b"/usr/local/bin/edenfsctl", + b"--takeover", + b"", + ] + instance = config_mod.eden_instance_from_cmdline(cmdline) + + for mount, initial_mount_protocol in mounts.items(): + checkoutConfig = config_mod.EdenCheckout( + instance, self.tmp_dir / mount, self._state_dir / "clients" / mount + ).get_config() + + self.assertEqual(checkoutConfig.mount_protocol, initial_mount_protocol) + + config_mod._do_nfs_migration(instance, lambda protocol: protocol) + + for mount in mounts: + checkoutConfig = config_mod.EdenCheckout( + instance, self.tmp_dir / mount, self._state_dir / "clients" / mount + ).get_config() + + self.assertEqual(checkoutConfig.mount_protocol, "nfs") + + def test_none(self) -> None: + mounts = {} + self.check_migrate_nfs(mounts) + + def test_simple(self) -> None: + mounts = {"test": "fuse"} + self.check_migrate_nfs(mounts) + + def test_multiple(self) -> None: + mounts = {"test1": "fuse", "test2": "fuse"} + self.check_migrate_nfs(mounts) + + def test_already_nfs(self) -> None: + mounts = {"test": "nfs"} + self.check_migrate_nfs(mounts) + + def test_multiple_nfs_fuse(self) -> None: + mounts = {"test1": "nfs", "test2": "fuse", "test3": "fuse", "test4": "nfs"} + self.check_migrate_nfs(mounts) diff --git a/eden/fs/cli/util.py b/eden/fs/cli/util.py index 012c9d25e9..ac492f5313 100644 --- a/eden/fs/cli/util.py +++ b/eden/fs/cli/util.py @@ -39,6 +39,10 @@ if sys.platform != "win32": LOCK_FILE = "lock" PID_FILE = "pid" +NFS_MOUNT_PROTOCOL_STRING = "nfs" +FUSE_MOUNT_PROTOCOL_STRING = "fuse" +PRJFS_MOUNT_PROTOCOL_STRING = "prjfs" + class EdenStartError(Exception): pass @@ -711,9 +715,9 @@ def is_apple_silicon() -> bool: def get_protocol(nfs: bool) -> str: if sys.platform == "win32": - return "prjfs" + return PRJFS_MOUNT_PROTOCOL_STRING else: - return "nfs" if nfs else "fuse" + return NFS_MOUNT_PROTOCOL_STRING if nfs else FUSE_MOUNT_PROTOCOL_STRING def get_tip_commit_hash(repo: Path) -> bytes: