BuildAPI: Implement UnmountPath method in sdk/service.

BUG=chromium:1095661
TEST=manual, run_tests

Change-Id: I450d1c94e7b2b1d33c30e0bd571fb8e4deb5c353
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/chromite/+/2327231
Tested-by: Michael Mortensen <mmortensen@google.com>
Commit-Queue: Michael Mortensen <mmortensen@google.com>
Reviewed-by: Alex Klein <saklein@chromium.org>
diff --git a/api/controller/sdk_unittest.py b/api/controller/sdk_unittest.py
index c6b7ceb..afda466 100644
--- a/api/controller/sdk_unittest.py
+++ b/api/controller/sdk_unittest.py
@@ -195,7 +195,6 @@
     sdk_controller.UnmountPath(request, self.response, self.api_config)
     # Verify that by default sdk_service.Delete is called with force=True.
     patch.assert_called_once_with(mock.ANY)
-    # TODO(crbug/1095661): Update the test once the service is implemented.
 
 
 class SdkUpdateTest(cros_test_lib.MockTestCase, api_config.ApiConfigMixin):
diff --git a/service/sdk.py b/service/sdk.py
index 9659eae..ef9aef7 100644
--- a/service/sdk.py
+++ b/service/sdk.py
@@ -14,6 +14,20 @@
 from chromite.lib import cros_build_lib
 from chromite.lib import cros_logging as logging
 from chromite.lib import cros_sdk_lib
+from chromite.lib import osutils
+
+
+
+class Error(Exception):
+  """Base module error."""
+
+
+class UnmountError(Error):
+  """An error raised when unmount fails."""
+
+  def __init__(self, message):
+    self.message = message
+    super(UnmountError, self).__init__(message)
 
 
 class CreateArguments(object):
@@ -208,12 +222,19 @@
 
 
 def UnmountPath(path):
-  """Unmount the chroot.
+  """Unmount the specified path.
 
   Args:
     path (chromiumos.Path.path): The path being unmounted.
   """
-  logging.info('Unmounting path %s', path)
+  logging.info('Unmounting path %s', path.path)
+  try:
+    osutils.UmountTree(path.path)
+  except cros_build_lib.RunCommandError as e:
+    fs_debug = cros_sdk_lib.GetFileSystemDebug(path.path, run_ps=True)
+    msg = (f'Umount failed: {e.result.error}.\nfuser output={fs_debug.fuser}\n'
+           f'lsof output={fs_debug.lsof}\nps output=fs_debug.ps\n')
+    raise UnmountError(msg)
 
 
 def GetChrootVersion(chroot_path=None):
diff --git a/service/sdk_unittest.py b/service/sdk_unittest.py
index cf430ee..d781e2f 100644
--- a/service/sdk_unittest.py
+++ b/service/sdk_unittest.py
@@ -9,9 +9,11 @@
 
 import os
 
+from chromite.api.gen.chromiumos import common_pb2
 from chromite.lib import chroot_lib
 from chromite.lib import cros_build_lib
 from chromite.lib import cros_test_lib
+from chromite.lib import osutils
 from chromite.service import sdk
 
 
@@ -77,6 +79,25 @@
         [], self._GetArgList(build_source=False, toolchain_targets=None))
 
 
+class UnmountTest(cros_test_lib.RunCommandTempDirTestCase,
+                  cros_test_lib.MockTestCase):
+  """Unmount tests."""
+
+  def testUnmountPath(self):
+    path_proto = common_pb2.Path(path='/some/path')
+    self.PatchObject(osutils, 'UmountTree', return_value=True)
+    sdk.UnmountPath(path_proto)
+
+  def testUnmountPathFails(self):
+    path_proto = common_pb2.Path(path='/some/path')
+    self.PatchObject(osutils, 'UmountTree',
+                     side_effect=cros_build_lib.RunCommandError(
+                         'umount failure'))
+    with self.assertRaises(sdk.UnmountError) as unmount_assert:
+      sdk.UnmountPath(path_proto)
+    self.assertIn('Umount failed:', unmount_assert.exception.message)
+
+
 class CreateTest(cros_test_lib.RunCommandTempDirTestCase):
   """Create function tests."""