Merge "Make delete a little bit handier."
diff --git a/Android.bp b/Android.bp
index 8eadb08..843fcba 100644
--- a/Android.bp
+++ b/Android.bp
@@ -40,6 +40,7 @@
],
libs: [
"acloud_create",
+ "acloud_delete",
"acloud_public",
"acloud_internal",
"acloud_proto",
@@ -67,6 +68,7 @@
],
libs: [
"acloud_create",
+ "acloud_delete",
"acloud_internal",
"acloud_proto",
"acloud_public",
@@ -136,3 +138,11 @@
"create/*.py",
],
}
+
+python_library_host{
+ name: "acloud_delete",
+ defaults: ["acloud_default"],
+ srcs: [
+ "delete/*.py",
+ ],
+}
diff --git a/create/create_common.py b/create/create_common.py
index 7f1c094..be67a2d 100644
--- a/create/create_common.py
+++ b/create/create_common.py
@@ -20,7 +20,6 @@
import glob
import logging
import os
-import sys
from acloud import errors
from acloud.internal import constants
@@ -64,41 +63,6 @@
return hw_dict
-def GetAnswerFromList(answer_list):
- """Get answer from a list.
-
- Args:
- answer_list: list of the answers to choose from.
-
- Return:
- String of the answer.
-
- Raises:
- error.ChoiceExit: User choice exit.
- """
- print("[0] to exit.")
- for num, item in enumerate(answer_list, 1):
- print("[%d] %s" % (num, item))
-
- choice = -1
- max_choice = len(answer_list)
- while True:
- try:
- choice = raw_input("Enter your choice[0-%d]: " % max_choice)
- choice = int(choice)
- except ValueError:
- print("'%s' is not a valid integer.", choice)
- continue
- # Filter out choices
- if choice == 0:
- print("Exiting acloud.")
- sys.exit()
- if choice < 0 or choice > max_choice:
- print("please choose between 0 and %d" % max_choice)
- else:
- return answer_list[choice-1]
-
-
def VerifyLocalImageArtifactsExist(local_image_dir):
"""Verify the specifies local image dir.
@@ -125,7 +89,7 @@
(image_pattern, local_image_dir))
if len(images) > 1:
print("Multiple images found, please choose 1.")
- image_path = GetAnswerFromList(images)
+ image_path = utils.GetAnswerFromList(images)[0]
else:
image_path = images[0]
logger.debug("Local image: %s ", image_path)
diff --git a/create/create_common_test.py b/create/create_common_test.py
index bb7cf13..25b193a 100644
--- a/create/create_common_test.py
+++ b/create/create_common_test.py
@@ -61,21 +61,6 @@
create_common.VerifyLocalImageArtifactsExist("/fake_dirs"),
"/fake_dirs/aosp_cf_x86_phone-img-5046769.zip")
- @mock.patch("__builtin__.raw_input")
- def testGetAnswerFromList(self, mock_raw_input):
- """Test GetAnswerFromList."""
- answer_list = ["image1.zip", "image2.zip", "image3.zip"]
- mock_raw_input.return_value = 0
- with self.assertRaises(SystemExit):
- create_common.GetAnswerFromList(answer_list)
- mock_raw_input.side_effect = [1, 2, 3]
- self.assertEqual(create_common.GetAnswerFromList(answer_list),
- "image1.zip")
- self.assertEqual(create_common.GetAnswerFromList(answer_list),
- "image2.zip")
- self.assertEqual(create_common.GetAnswerFromList(answer_list),
- "image3.zip")
-
if __name__ == "__main__":
unittest.main()
diff --git a/delete/__init__.py b/delete/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/delete/__init__.py
diff --git a/delete/delete.py b/delete/delete.py
new file mode 100644
index 0000000..f927489
--- /dev/null
+++ b/delete/delete.py
@@ -0,0 +1,132 @@
+#!/usr/bin/env python
+#
+# Copyright 2018 - 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"""Delete entry point.
+
+Delete will handle all the logic related to deleting a local/remote instance
+of an Android Virtual Device.
+"""
+
+from __future__ import print_function
+import getpass
+import logging
+import time
+
+from acloud.internal.lib import auth
+from acloud.internal.lib import gcompute_client
+from acloud.internal.lib import utils
+from acloud.public import config
+from acloud.public import device_driver
+
+logger = logging.getLogger(__name__)
+
+_COMPUTE_SCOPE = "https://www.googleapis.com/auth/compute",
+
+
+def _FilterInstancesByUser(instances, user):
+ """Look through the instance data and filter them.
+
+ Args:
+ user: String, username to filter on.
+ instances: List of instance data. This is the response from the GCP
+ compute API for listing instances.
+
+ Returns:
+ List of strings of the instance names.
+ """
+ filtered_instances = []
+ for instance_info in instances:
+ instance_name = instance_info.get("name")
+ for metadata in instance_info.get("metadata", {}).get("items", []):
+ if metadata["key"] == "user" and metadata["value"] == user:
+ filtered_instances.append(instance_name)
+ return filtered_instances
+
+
+def _FindRemoteInstances(cfg, user):
+ """Find instances created by user in project specified in cfg.
+
+ Args:
+ cfg: AcloudConfig object.
+ user: String, username to look for.
+
+ Returns:
+ List of strings that are instance names.
+ """
+ credentials = auth.CreateCredentials(cfg, _COMPUTE_SCOPE)
+ compute_client = gcompute_client.ComputeClient(cfg, credentials)
+ all_instances = compute_client.ListInstances(cfg.zone)
+ return _FilterInstancesByUser(all_instances, user)
+
+
+def _DeleteRemoteInstances(args, del_all_instances=False):
+ """Look for remote instances and delete them.
+
+ We're going to query the GCP project for all instances that have the user
+ mentioned in the metadata. If we find just one instance, print out the
+ details of it and delete it. If we find more than 1, ask the user which one
+ they'd like to delete unless del_all_instances is True, then just delete
+ them all.
+
+ Args:
+ args: Namespace object from argparse.parse_args.
+ del_all_instances: Boolean, when more than 1 instance is found,
+ delete them all if True, otherwise prompt user.
+ """
+ cfg = config.GetAcloudConfig(args)
+ instances_to_delete = args.instance_names
+ if instances_to_delete is None:
+ instances_to_delete = _FindRemoteInstances(cfg, getpass.getuser())
+ if instances_to_delete:
+ # If the user didn't specify any instances and we find more than 1, ask
+ # them what they want to do (unless they specified --all).
+ if (args.instance_names is None
+ and len(instances_to_delete) > 1
+ and not del_all_instances):
+ print("Multiple instance detected, choose 1 to delete:")
+ instances_to_delete = utils.GetAnswerFromList(
+ instances_to_delete, enable_choose_all=True)
+ # TODO(b/117474343): We should do a couple extra things here:
+ # - adb disconnect
+ # - kill ssh tunnel and ssvnc
+ # - give details of each instance
+ # - Give better messaging about delete.
+ start = time.time()
+ utils.PrintColorString("Deleting %s ..." %
+ ", ".join(instances_to_delete),
+ utils.TextColors.WARNING, end="")
+ report = device_driver.DeleteAndroidVirtualDevices(cfg,
+ instances_to_delete)
+ if report.errors:
+ utils.PrintColorString("Fail! (%ds)" % (time.time() - start),
+ utils.TextColors.FAIL)
+ logger.debug("Delete failed: %s", report.errors)
+ else:
+ utils.PrintColorString("OK! (%ds)" % (time.time() - start),
+ utils.TextColors.OKGREEN)
+ return report
+ print("No instances to delete")
+ return None
+
+
+def Run(args):
+ """Run delete.
+
+ Args:
+ args: Namespace object from argparse.parse_args.
+ """
+ report = _DeleteRemoteInstances(args, args.all)
+ # TODO(b/117474343): Delete local instances.
+ return report
diff --git a/delete/delete_args.py b/delete/delete_args.py
new file mode 100644
index 0000000..e5ee438
--- /dev/null
+++ b/delete/delete_args.py
@@ -0,0 +1,50 @@
+#!/usr/bin/env python
+#
+# Copyright 2018 - 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"""Delete args.
+
+Defines the delete arg parser that holds delete specific args.
+"""
+
+CMD_DELETE = "delete"
+
+
+def GetDeleteArgParser(subparser):
+ """Return the delete arg parser.
+
+ Args:
+ subparser: argparse.ArgumentParser that is attached to main acloud cmd.
+
+ Returns:
+ argparse.ArgumentParser with delete options defined.
+ """
+ delete_parser = subparser.add_parser(CMD_DELETE)
+ delete_parser.required = False
+ delete_parser.set_defaults(which=CMD_DELETE)
+ delete_parser.add_argument(
+ "--instance_names",
+ dest="instance_names",
+ nargs="+",
+ required=False,
+ help="The names of the instances that need to delete, "
+ "separated by spaces, e.g. --instance_names instance-1 instance-2")
+ delete_parser.add_argument(
+ "--all",
+ action="store_true",
+ dest="all",
+ required=False,
+ help="If more than 1 AVD instance is found, delete them all.")
+
+ return delete_parser
diff --git a/delete/delete_test.py b/delete/delete_test.py
new file mode 100644
index 0000000..adac5f1
--- /dev/null
+++ b/delete/delete_test.py
@@ -0,0 +1,43 @@
+# Copyright 2018 - 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 delete."""
+
+import unittest
+
+from acloud.delete import delete
+
+
+# pylint: disable=invalid-name,protected-access
+class DeleteTest(unittest.TestCase):
+ """Test delete functions."""
+
+ # pylint: disable=protected-access
+ def testFilterInstancesByUser(self):
+ """Test _FilterInstancesByUser."""
+ user = "instance_match_user"
+ matched_instance = "instance_1"
+ instances = [
+ {"name": matched_instance,
+ "metadata": {"items": [{"key": "user",
+ "value": user}]}},
+ {"name": "instance_2",
+ "metadata": {"items": [{"key": "user",
+ "value": "instance_no_match_user"}]}}]
+ expected_instances = [matched_instance]
+ self.assertEqual(expected_instances,
+ delete._FilterInstancesByUser(instances, user))
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/internal/lib/utils.py b/internal/lib/utils.py
index c832f3b..4acacbc 100755
--- a/internal/lib/utils.py
+++ b/internal/lib/utils.py
@@ -715,3 +715,41 @@
return ForwardedPorts(vnc_port=local_free_vnc_port,
adb_port=local_free_adb_port)
+
+
+def GetAnswerFromList(answer_list, enable_choose_all=False):
+ """Get answer from a list.
+
+ Args:
+ answer_list: list of the answers to choose from.
+
+ Return:
+ List holding the answer(s).
+ """
+ print("[0] to exit.")
+ start_index = 1
+ if enable_choose_all:
+ start_index = 2
+ print("[1] for all.")
+ for num, item in enumerate(answer_list, start_index):
+ print("[%d] %s" % (num, item))
+
+ choice = -1
+ max_choice = len(answer_list) + 1
+ while True:
+ try:
+ choice = raw_input("Enter your choice[0-%d]: " % max_choice)
+ choice = int(choice)
+ except ValueError:
+ print("'%s' is not a valid integer.", choice)
+ continue
+ # Filter out choices
+ if choice == 0:
+ print("Exiting acloud.")
+ sys.exit()
+ if enable_choose_all and choice == 1:
+ return answer_list
+ if choice < 0 or choice > max_choice:
+ print("please choose between 0 and %d" % max_choice)
+ else:
+ return [answer_list[choice-start_index]]
diff --git a/internal/lib/utils_test.py b/internal/lib/utils_test.py
index aad23c1..9d62077 100644
--- a/internal/lib/utils_test.py
+++ b/internal/lib/utils_test.py
@@ -226,6 +226,24 @@
mock.call(16)
])
+ @mock.patch("__builtin__.raw_input")
+ def testGetAnswerFromList(self, mock_raw_input):
+ """Test GetAnswerFromList."""
+ answer_list = ["image1.zip", "image2.zip", "image3.zip"]
+ mock_raw_input.return_value = 0
+ with self.assertRaises(SystemExit):
+ utils.GetAnswerFromList(answer_list)
+ mock_raw_input.side_effect = [1, 2, 3, 1]
+ self.assertEqual(utils.GetAnswerFromList(answer_list),
+ ["image1.zip"])
+ self.assertEqual(utils.GetAnswerFromList(answer_list),
+ ["image2.zip"])
+ self.assertEqual(utils.GetAnswerFromList(answer_list),
+ ["image3.zip"])
+ self.assertEqual(utils.GetAnswerFromList(answer_list,
+ enable_choose_all=True),
+ answer_list)
+
if __name__ == "__main__":
unittest.main()
diff --git a/public/acloud_main.py b/public/acloud_main.py
index f548789..1375236 100644
--- a/public/acloud_main.py
+++ b/public/acloud_main.py
@@ -86,6 +86,8 @@
from acloud.public.actions import create_goldfish_action
from acloud.create import create
from acloud.create import create_args
+from acloud.delete import delete
+from acloud.delete import delete_args
from acloud.setup import setup
from acloud.setup import setup_args
@@ -114,9 +116,9 @@
CMD_CLEANUP,
CMD_CREATE_CUTTLEFISH,
CMD_CREATE_GOLDFISH,
- CMD_DELETE,
CMD_SSHKEY,
create_args.CMD_CREATE,
+ delete_args.CMD_DELETE,
setup_args.CMD_SETUP,
])
parser = argparse.ArgumentParser(
@@ -126,9 +128,6 @@
subparsers = parser.add_subparsers()
subparser_list = []
- # Command "create"
- subparser_list.append(create_args.GetCreateArgParser(subparsers))
-
# Command "create_cf", create cuttlefish instances
create_cf_parser = subparsers.add_parser(CMD_CREATE_CUTTLEFISH)
create_cf_parser.required = False
@@ -211,19 +210,6 @@
create_args.AddCommonCreateArgs(create_gf_parser)
subparser_list.append(create_gf_parser)
- # Command "Delete"
- delete_parser = subparsers.add_parser(CMD_DELETE)
- delete_parser.required = False
- delete_parser.set_defaults(which=CMD_DELETE)
- delete_parser.add_argument(
- "--instance_names",
- dest="instance_names",
- nargs="+",
- required=True,
- help="The names of the instances that need to delete, "
- "separated by spaces, e.g. --instance_names instance-1 instance-2")
- subparser_list.append(delete_parser)
-
# Command "cleanup"
cleanup_parser = subparsers.add_parser(CMD_CLEANUP)
cleanup_parser.required = False
@@ -257,9 +243,15 @@
"that will be added as project-wide ssh key.")
subparser_list.append(sshkey_parser)
+ # Command "create"
+ subparser_list.append(create_args.GetCreateArgParser(subparsers))
+
# Command "setup"
subparser_list.append(setup_args.GetSetupArgParser(subparsers))
+ # Command "Delete"
+ subparser_list.append(delete_args.GetDeleteArgParser(subparsers))
+
# Add common arguments.
for subparser in subparser_list:
acloud_common.AddCommonArguments(subparser)
@@ -462,8 +454,7 @@
branch=args.branch,
report_internal_ip=args.report_internal_ip)
elif args.which == CMD_DELETE:
- report = device_driver.DeleteAndroidVirtualDevices(
- cfg, args.instance_names)
+ report = delete.Run(args)
elif args.which == CMD_CLEANUP:
report = device_driver.Cleanup(cfg, args.expiration_mins)
elif args.which == CMD_SSHKEY: