acloud: cleanup host to a pristine state.

The feature  will cleanup the packages installed by acloud setup --host

Bug: 145763747
Bug: 191308624
Test: acloud-dev hostcleanup

Change-Id: I06ea47776e3c013f0aefd713ff2b283d10a23e46
diff --git a/Android.bp b/Android.bp
index ec2c2c1..c6e7534 100644
--- a/Android.bp
+++ b/Android.bp
@@ -74,6 +74,7 @@
         "acloud_public",
         "acloud_restart",
         "acloud_setup",
+        "acloud_hostcleanup",
         "py-apitools",
         "py-dateutil",
         "py-google-api-python-client",
@@ -112,6 +113,7 @@
         "acloud_proto",
         "acloud_restart",
         "acloud_setup",
+        "acloud_hostcleanup",
         "asuite_cc_client",
         "py-apitools",
         "py-dateutil",
@@ -232,6 +234,14 @@
 }
 
 python_library_host{
+    name: "acloud_hostcleanup",
+    defaults: ["acloud_default"],
+    srcs: [
+         "hostcleanup/*.py",
+    ],
+}
+
+python_library_host{
     name: "acloud_metrics",
     defaults: ["acloud_default"],
     srcs: [
diff --git a/hostcleanup/__init__.py b/hostcleanup/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/hostcleanup/__init__.py
diff --git a/hostcleanup/host_cleanup_runner.py b/hostcleanup/host_cleanup_runner.py
new file mode 100644
index 0000000..22b5c14
--- /dev/null
+++ b/hostcleanup/host_cleanup_runner.py
@@ -0,0 +1,125 @@
+# Copyright 2021 - The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+r"""host cleanup runner
+
+A host cleanup sub task runner will cleanup host to a pristine state.
+"""
+
+from __future__ import print_function
+
+import logging
+import os
+import subprocess
+import textwrap
+
+from acloud.internal import constants
+from acloud.internal.lib import utils
+from acloud.setup import base_task_runner
+from acloud.setup import setup_common
+
+logger = logging.getLogger(__name__)
+
+_PARAGRAPH_BREAK = "="
+_PURGE_PACKAGE_CMD = "sudo apt-get purge --assume-yes %s"
+_UNINSTALL_SUCCESS_MSG = "Package(s) [%s] have uninstalled."
+
+
+class BasePurger(base_task_runner.BaseTaskRunner):
+    """Subtask base runner class for hostcleanup."""
+
+    PURGE_MESSAGE_TITLE = ""
+    PURGE_MESSAGE = ""
+
+    cmds = []
+    purge_packages = []
+
+    def ShouldRun(self):
+        """Check if required packages are all uninstalled.
+
+        Returns:
+            Boolean, True if command list not null.
+        """
+        if not utils.IsSupportedPlatform():
+            return False
+
+        if self.cmds:
+            return True
+
+        utils.PrintColorString(
+            "[%s]: don't have to process." % self.PURGE_MESSAGE_TITLE,
+            utils.TextColors.WARNING)
+        return False
+
+    def _Run(self):
+        """Run purge commands."""
+        utils.PrintColorString("Below commands will be run: \n%s" %
+                               "\n".join(self.cmds))
+
+        answer_client = utils.InteractWithQuestion(
+            "\nPress 'y' to continue or anything else to do it myself[y/N]: ",
+            utils.TextColors.WARNING)
+        if answer_client not in constants.USER_ANSWER_YES:
+            return
+
+        for cmd in self.cmds:
+            try:
+                setup_common.CheckCmdOutput(cmd,
+                                            shell=True,
+                                            stderr=subprocess.STDOUT)
+            except subprocess.CalledProcessError as cpe:
+                logger.error("Run command [%s] failed: %s",
+                             cmd, cpe.output)
+
+        utils.PrintColorString((_UNINSTALL_SUCCESS_MSG %
+                                ",".join(self.purge_packages)),
+                               utils.TextColors.OKGREEN)
+
+    def PrintPurgeMessage(self):
+        """Print purge message"""
+        # define the layout of message.
+        console_width = int(os.popen('stty size', 'r').read().split()[1])
+        break_width = int(console_width / 2)
+
+        # start to print purge message.
+        print("\n" + _PARAGRAPH_BREAK * break_width)
+        print(" [%s] " % self.PURGE_MESSAGE_TITLE)
+        print(textwrap.fill(
+            self.PURGE_MESSAGE,
+            break_width - 2,
+            initial_indent=" ",
+            subsequent_indent=" "))
+        print(_PARAGRAPH_BREAK * break_width + "\n")
+
+
+class PackagesUninstaller(BasePurger):
+    """Subtask base runner class for uninstalling packages."""
+
+    PURGE_MESSAGE_TITLE = "Uninstalling packages"
+    PURGE_MESSAGE = ("This will uninstall packages installed previously "
+                     "through \"acloud setup --host-setup\"")
+
+    def __init__(self):
+        """Initialize."""
+        packages = []
+        packages.extend(constants.AVD_REQUIRED_PKGS)
+        packages.extend(constants.BASE_REQUIRED_PKGS)
+        packages.append(constants.CUTTLEFISH_COMMOM_PKG)
+
+        self.purge_packages = [pkg for pkg in packages
+                               if setup_common.PackageInstalled(pkg)]
+
+        self.cmds = [
+            _PURGE_PACKAGE_CMD % pkg for pkg in self.purge_packages]
+
+        self.PrintPurgeMessage()
diff --git a/hostcleanup/hostcleanup.py b/hostcleanup/hostcleanup.py
new file mode 100644
index 0000000..56b03c9
--- /dev/null
+++ b/hostcleanup/hostcleanup.py
@@ -0,0 +1,31 @@
+# Copyright 2021 - The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+r"""Hostcleanup entry point.
+
+Hostcleanup will rollback acloud host setup steps.
+"""
+from acloud.hostcleanup import host_cleanup_runner
+
+def Run(args):
+    """Run Hostcleanup.
+
+    Hostcleanup options:
+        -cleanup_pkgs: Uninstall packages.
+
+    Args:
+        args: Namespace object from argparse.parse_args.
+    """
+    # TODO(b/145763747): Need to implement cleanup configs and usergroup.
+    if args.cleanup_pkgs:
+        host_cleanup_runner.PackagesUninstaller().Run()
diff --git a/hostcleanup/hostcleanup_args.py b/hostcleanup/hostcleanup_args.py
new file mode 100644
index 0000000..15c4a92
--- /dev/null
+++ b/hostcleanup/hostcleanup_args.py
@@ -0,0 +1,42 @@
+# Copyright 2021 - The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+r"""hostcleanup args.
+
+Defines the hostcleanup arg parser.
+"""
+
+CMD_HOSTCLEANUP = "hostcleanup"
+
+
+def GetHostcleanupArgParser(subparser):
+    """Return the hostcleanup arg parser.
+
+    Args:
+        subparser: argparse.ArgumentParser that is attached to main acloud cmd.
+
+    Returns:
+        argparse.ArgumentParser with hostcleanup options defined.
+    """
+    hostcleanup_parser = subparser.add_parser(CMD_HOSTCLEANUP)
+    hostcleanup_parser.required = False
+    hostcleanup_parser.set_defaults(which=CMD_HOSTCLEANUP)
+    hostcleanup_parser.add_argument(
+        "--packages",
+        action="store_true",
+        dest="cleanup_pkgs",
+        required=False,
+        default=True,
+        help="This feature will purge all packages installed by the acloud.")
+
+    return hostcleanup_parser
diff --git a/hostcleanup/hostcleanup_test.py b/hostcleanup/hostcleanup_test.py
new file mode 100644
index 0000000..d24692d
--- /dev/null
+++ b/hostcleanup/hostcleanup_test.py
@@ -0,0 +1,37 @@
+# Copyright 2021 - The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Tests for hostcleanup."""
+import unittest
+
+from unittest import mock
+
+from acloud.internal.lib import driver_test_lib
+from acloud.hostcleanup import hostcleanup
+from acloud.hostcleanup import host_cleanup_runner
+
+
+class HostcleanupTest(driver_test_lib.BaseDriverTest):
+    """Test hostcleanup."""
+
+    # pylint: disable=no-self-use
+    @mock.patch.object(host_cleanup_runner, "PackagesUninstaller")
+    def testRun(self, mock_uninstallpkgs):
+        """test Run."""
+        args = mock.MagicMock()
+        hostcleanup.Run(args)
+        mock_uninstallpkgs.assert_called_once()
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/internal/constants.py b/internal/constants.py
index cca3f94..7b24056 100755
--- a/internal/constants.py
+++ b/internal/constants.py
@@ -233,3 +233,10 @@
 
 # The name of download image tool.
 FETCH_CVD = "fetch_cvd"
+
+# For setup and cleanup
+# Packages "devscripts" and "equivs" are required for "mk-build-deps".
+AVD_REQUIRED_PKGS = [
+    "devscripts", "equivs", "libvirt-clients", "libvirt-daemon-system"]
+BASE_REQUIRED_PKGS = ["ssvnc", "lzop", "python3-tk"]
+CUTTLEFISH_COMMOM_PKG = "cuttlefish-common"
diff --git a/public/acloud_main.py b/public/acloud_main.py
index 8102136..fd094d9 100644
--- a/public/acloud_main.py
+++ b/public/acloud_main.py
@@ -136,6 +136,8 @@
 from acloud.restart import restart_args
 from acloud.setup import setup
 from acloud.setup import setup_args
+from acloud.hostcleanup import hostcleanup
+from acloud.hostcleanup import hostcleanup_args
 
 
 LOGGING_FMT = "%(asctime)s |%(levelname)s| %(module)s:%(lineno)s| %(message)s"
@@ -177,6 +179,7 @@
         reconnect_args.CMD_RECONNECT,
         pull_args.CMD_PULL,
         restart_args.CMD_RESTART,
+        hostcleanup_args.CMD_HOSTCLEANUP,
     ])
     parser = argparse.ArgumentParser(
         description=__doc__,
@@ -260,6 +263,9 @@
     # Command "pull"
     subparser_list.append(pull_args.GetPullArgParser(subparsers))
 
+    # Command "hostcleanup"
+    subparser_list.append(hostcleanup_args.GetHostcleanupArgParser(subparsers))
+
     # Add common arguments.
     for subparser in subparser_list:
         acloud_common.AddCommonArguments(subparser)
@@ -476,6 +482,8 @@
         reporter = pull.Run(args)
     elif args.which == setup_args.CMD_SETUP:
         setup.Run(args)
+    elif args.which == hostcleanup_args.CMD_HOSTCLEANUP:
+        hostcleanup.Run(args)
     else:
         error_msg = "Invalid command %s" % args.which
         sys.stderr.write(error_msg)
diff --git a/setup/host_setup_runner.py b/setup/host_setup_runner.py
index b1c1e14..c3cbad0 100644
--- a/setup/host_setup_runner.py
+++ b/setup/host_setup_runner.py
@@ -36,11 +36,6 @@
 
 logger = logging.getLogger(__name__)
 
-# Packages "devscripts" and "equivs" are required for "mk-build-deps".
-_AVD_REQUIRED_PKGS = [
-    "devscripts", "equivs", "libvirt-clients", "libvirt-daemon-system"]
-_BASE_REQUIRED_PKGS = ["ssvnc", "lzop", "python3-tk"]
-_CUTTLEFISH_COMMOM_PKG = "cuttlefish-common"
 _CF_COMMOM_FOLDER = "cf-common"
 _LIST_OF_MODULES = ["kvm_intel", "kvm"]
 _UPDATE_APT_GET_CMD = "sudo apt-get update"
@@ -113,7 +108,7 @@
     WELCOME_MESSAGE = ("This step will walk you through the required packages "
                        "installation for running Android cuttlefish devices "
                        "on your host.")
-    PACKAGES = _AVD_REQUIRED_PKGS
+    PACKAGES = constants.AVD_REQUIRED_PKGS
 
 
 class HostBasePkgInstaller(BasePkgInstaller):
@@ -122,7 +117,7 @@
     WELCOME_MESSAGE_TITLE = "Install base packages on the host"
     WELCOME_MESSAGE = ("This step will walk you through the base packages "
                        "installation for your host.")
-    PACKAGES = _BASE_REQUIRED_PKGS
+    PACKAGES = constants.BASE_REQUIRED_PKGS
 
 
 class CuttlefishCommonPkgInstaller(base_task_runner.BaseTaskRunner):
@@ -143,7 +138,7 @@
 
         # Any required package is not installed or not up-to-date will need to
         # run installation task.
-        if not setup_common.PackageInstalled(_CUTTLEFISH_COMMOM_PKG):
+        if not setup_common.PackageInstalled(constants.CUTTLEFISH_COMMOM_PKG):
             return True
         return False