sdk_subtools: Refactor into a build api endpoint.

Methods from scripts/build_sdk_subtools that are shared with the recipe
are moved mostly verbatim into chromite/service/sdk_subtools.py.

A `sudo` argument is added to setup_base_sdk, and the subtools chroot
sentinel file is created with osutils.WriteText(sudo=True) to reduce
friction when invoking from the build api layer.

api/controller/sdk_subtools.py is added with unit tests and a
call_scripts template, and the proto is registered with the router.

BUG=b:277992359
TEST=call_scripts/build_sdk_subtools__build_sdk_subtools

Change-Id: I5907e1a92050b0d781962eb4812112efe41b5684
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/chromite/+/4792625
Reviewed-by: Mike Frysinger <vapier@chromium.org>
Commit-Queue: Trent Apted <tapted@chromium.org>
Tested-by: Trent Apted <tapted@chromium.org>
diff --git a/scripts/build_sdk_subtools_unittest.py b/scripts/build_sdk_subtools_unittest.py
index 61919f4..07c9ff9 100644
--- a/scripts/build_sdk_subtools_unittest.py
+++ b/scripts/build_sdk_subtools_unittest.py
@@ -12,6 +12,7 @@
 from chromite.lib import commandline
 from chromite.lib import cros_build_lib
 from chromite.scripts import build_sdk_subtools
+from chromite.service import sdk_subtools
 
 
 @pytest.fixture(name="outside_chroot")
@@ -27,11 +28,11 @@
 def mock_emerge_fixture():
     """Stubs the build_sdk_subtools emerge helper and sets it up to run."""
     with mock.patch.multiple(
-        "chromite.scripts.build_sdk_subtools",
+        "chromite.service.sdk_subtools",
         _run_system_emerge=mock.DEFAULT,
-        _is_inside_subtools_chroot=mock.DEFAULT,
+        is_inside_subtools_chroot=mock.DEFAULT,
     ) as mocks:
-        mocks["_is_inside_subtools_chroot"].return_value = True
+        mocks["is_inside_subtools_chroot"].return_value = True
         yield mocks["_run_system_emerge"]
 
 
@@ -42,6 +43,19 @@
         yield mock_lib
 
 
+@pytest.fixture(autouse=True)
+def build_sdk_subtools_consistency_check():
+    """Die quickly if the version file is left over in the test SDK.
+
+    This can happen if the build API entrypoint was tested on the local machine.
+    Tests in this file will fail in confusing ways if this is ever the case.
+    """
+    version_file = sdk_subtools.SUBTOOLS_CHROOT_VERSION_FILE
+    assert (
+        not version_file.exists()
+    ), f"{version_file} exists in the chroot (stray?)."
+
+
 def test_must_run_outside_sdk(caplog) -> None:
     """Tests build_sdk_subtools complains if run in the chroot."""
     with pytest.raises(cros_build_lib.DieSystemExit) as error_info:
@@ -120,7 +134,7 @@
 
 def test_setup_sdk_invocation(run_mock, outside_chroot) -> None:
     """Tests the SDK setup invocation, before it becomes a subtools chroot."""
-    # Fake success from cros_sdk, failure from _setup_base_sdk().
+    # Fake success from cros_sdk, failure from setup_base_sdk().
     run_mock.SetDefaultCmdResult(returncode=0)
     run_mock.AddCmdResult(
         ["sudo", "--", "build_sdk_subtools", "--relaunch-for-setup"],