Add new option "gcp_init" for acloud setup
- Setup user information in gcloud config file
- Enable gcloud API service
- Generate SSH key
Bug: 110856450
Test: ./run_tests.sh, m acloud && acloud setup --gcp_init
acloud setup
Change-Id: I88d6e52cb5164e023023bf9c5c62b93c87c8bc3f
diff --git a/errors.py b/errors.py
index 11b6fcd..cdf1c3f 100644
--- a/errors.py
+++ b/errors.py
@@ -34,3 +34,7 @@
class NotSupportedPlatformError(SetupError):
"""Error related to user using a not supported os."""
+
+
+class ParseBucketRegionError(SetupError):
+ """Raised when parsing bucket information without region information."""
diff --git a/internal/lib/utils.py b/internal/lib/utils.py
index 4d32f8f..b15d46d 100755
--- a/internal/lib/utils.py
+++ b/internal/lib/utils.py
@@ -31,6 +31,7 @@
import time
import uuid
+from acloud.internal import constants
from acloud.public import errors
logger = logging.getLogger(__name__)
@@ -399,6 +400,19 @@
"""
return str(raw_input(colors + question + TextColors.ENDC).strip())
+def GetUserAnswerYes(question):
+ """Ask user about acloud setup question.
+
+ Args:
+ question: String, ask question for user.
+ Ex: "Are you sure to change bucket name:[y/n]"
+
+ Returns:
+ Boolean, True if answer is "Yes", False otherwise.
+ """
+ answer = InteractWithQuestion(question)
+ return answer.lower() in constants.USER_ANSWER_YES
+
class BatchHttpRequestExecutor(object):
"""A helper class that executes requests in batch with retry.
diff --git a/public/acloud_main.py b/public/acloud_main.py
index a4778c1..893d96d 100644
--- a/public/acloud_main.py
+++ b/public/acloud_main.py
@@ -463,7 +463,7 @@
elif args.which == CMD_SSHKEY:
report = device_driver.AddSshRsa(cfg, args.user, args.ssh_rsa_path)
elif args.which == setup_args.CMD_SETUP:
- setup.Run()
+ setup.Run(args)
else:
sys.stderr.write("Invalid command %s" % args.which)
return 2
diff --git a/public/config.py b/public/config.py
index e7c1b3f..2249ed0 100755
--- a/public/config.py
+++ b/public/config.py
@@ -221,7 +221,7 @@
user_config_path: path to the user config.
internal_config_path: path to the internal conifg.
"""
- self._user_config_path = user_config_path
+ self.user_config_path = user_config_path
self._internal_config_path = internal_config_path
def Load(self):
@@ -244,18 +244,18 @@
except OSError as e:
raise errors.ConfigError("Could not load config files: %s" % str(e))
# Load user config file
- if self._user_config_path:
- if os.path.exists(self._user_config_path):
- with open(self._user_config_path, "r") as config_file:
+ if self.user_config_path:
+ if os.path.exists(self.user_config_path):
+ with open(self.user_config_path, "r") as config_file:
usr_cfg = self.LoadConfigFromProtocolBuffer(
config_file, user_config_pb2.UserConfig)
else:
raise errors.ConfigError("The file doesn't exist: %s" %
- (self._user_config_path))
+ (self.user_config_path))
else:
- self._user_config_path = GetDefaultConfigFile()
- if os.path.exists(self._user_config_path):
- with open(self._user_config_path, "r") as config_file:
+ self.user_config_path = GetDefaultConfigFile()
+ if os.path.exists(self.user_config_path):
+ with open(self.user_config_path, "r") as config_file:
usr_cfg = self.LoadConfigFromProtocolBuffer(
config_file, user_config_pb2.UserConfig)
else:
diff --git a/public/config_test.py b/public/config_test.py
index b3061d6..781244b 100644
--- a/public/config_test.py
+++ b/public/config_test.py
@@ -138,14 +138,14 @@
"""
config_specify = config.AcloudConfigManager(self.config_file)
self.config_file.read.return_value = self.USER_CONFIG
- self.assertEqual(config_specify._user_config_path, self.config_file)
+ self.assertEqual(config_specify.user_config_path, self.config_file)
mock_file_exist.return_value = False
with self.assertRaises(errors.ConfigError):
config_specify.Load()
# Test default config
config_unspecify = config.AcloudConfigManager(None)
cfg = config_unspecify.Load()
- self.assertEqual(config_unspecify._user_config_path,
+ self.assertEqual(config_unspecify.user_config_path,
config.GetDefaultConfigFile())
self.assertEqual(cfg.project, "")
self.assertEqual(cfg.zone, "")
diff --git a/setup/gcp_setup_runner.py b/setup/gcp_setup_runner.py
new file mode 100644
index 0000000..7b9feb5
--- /dev/null
+++ b/setup/gcp_setup_runner.py
@@ -0,0 +1,486 @@
+#!/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.
+"""Gcloud setup runner."""
+
+from __future__ import print_function
+import logging
+import os
+import re
+import subprocess
+
+from acloud import errors
+from acloud.internal.lib import utils
+from acloud.public import config
+from acloud.setup import base_task_runner
+from acloud.setup import google_sdk
+
+# APIs that need to be enabled for GCP project.
+_ANDROID_BUILD_SERVICE = "androidbuildinternal.googleapis.com"
+_COMPUTE_ENGINE_SERVICE = "compute.googleapis.com"
+_GOOGLE_CLOUD_STORAGE_SERVICE = "storage-component.googleapis.com"
+_GOOGLE_APIS = [
+ _GOOGLE_CLOUD_STORAGE_SERVICE, _ANDROID_BUILD_SERVICE,
+ _COMPUTE_ENGINE_SERVICE
+]
+_BUILD_SERVICE_ACCOUNT = "android-build-prod@system.gserviceaccount.com"
+_BUCKET_HEADER = "gs://"
+_DEFAULT_BUCKET_HEADER = "acloud"
+_DEFAULT_BUCKET_REGION = "US"
+_DEFAULT_SSH_FOLDER = os.path.expanduser("~/.ssh")
+_DEFAULT_SSH_KEY = "acloud_rsa"
+_DEFAULT_SSH_PRIVATE_KEY = os.path.join(_DEFAULT_SSH_FOLDER,
+ _DEFAULT_SSH_KEY)
+_DEFAULT_SSH_PUBLIC_KEY = os.path.join(_DEFAULT_SSH_FOLDER,
+ _DEFAULT_SSH_KEY + ".pub")
+# Regular expression to get project/zone/bucket information.
+_BUCKET_RE = re.compile(r"^gs://(?P<bucket>.+)/")
+_BUCKET_REGION_RE = re.compile(r"^Location constraint:(?P<region>.+)")
+_PROJECT_RE = re.compile(r"^project = (?P<project>.+)")
+_ZONE_RE = re.compile(r"^zone = (?P<zone>.+)")
+
+logger = logging.getLogger(__name__)
+
+
+def UpdateConfigFile(config_path, item, value):
+ """Update config data.
+
+ Case A: config file contain this item.
+ In config, "project = A_project". New value is B_project
+ Set config "project = B_project".
+ Case B: config file didn't contain this item.
+ New value is B_project.
+ Setup config as "project = B_project".
+
+ Args:
+ config_path: String, acloud config path.
+ item: String, item name in config file. EX: project, zone
+ value: String, value of item in config file.
+
+ TODO(111574698): Refactor this to minimize writes to the config file.
+ TODO(111574698): Use proto method to update config.
+ """
+ write_lines = []
+ find_item = False
+ write_line = item + ": \"" + value + "\"\n"
+ if os.path.isfile(config_path):
+ with open(config_path, "r") as cfg_file:
+ for read_line in cfg_file.readlines():
+ if read_line.startswith(item + ":"):
+ find_item = True
+ write_lines.append(write_line)
+ else:
+ write_lines.append(read_line)
+ if not find_item:
+ write_lines.append(write_line)
+ with open(config_path, "w") as cfg_file:
+ cfg_file.writelines(write_lines)
+
+
+def SetupSSHKeys(config_path, private_key_path, public_key_path):
+ """Setup the pair of the ssh key for acloud.config.
+
+ User can use the default path: "~/.ssh/acloud_rsa".
+
+ Args:
+ config_path: String, acloud config path.
+ private_key_path: Path to the private key file.
+ e.g. ~/.ssh/acloud_rsa
+ public_key_path: Path to the public key file.
+ e.g. ~/.ssh/acloud_rsa.pub
+ """
+ private_key_path = os.path.expanduser(private_key_path)
+ if (private_key_path == "" or public_key_path == ""
+ or private_key_path == _DEFAULT_SSH_PRIVATE_KEY):
+ utils.CreateSshKeyPairIfNotExist(_DEFAULT_SSH_PRIVATE_KEY,
+ _DEFAULT_SSH_PUBLIC_KEY)
+ UpdateConfigFile(config_path, "ssh_private_key_path",
+ _DEFAULT_SSH_PRIVATE_KEY)
+ UpdateConfigFile(config_path, "ssh_public_key_path",
+ _DEFAULT_SSH_PUBLIC_KEY)
+
+
+def _InputIsEmpty(input_string):
+ """Check input string is empty.
+
+ Tool requests user to input client ID & client secret.
+ This basic check can detect user input is empty.
+
+ Args:
+ input_string: String, user input string.
+
+ Returns:
+ Boolean: True if input is empty, False otherwise.
+ """
+ if input_string is None:
+ return True
+ if input_string == "":
+ print("Please enter a non-empty value.")
+ return True
+ return False
+
+
+class GoogleSDKBins(object):
+ """Class to run tools in the Google SDK."""
+
+ def __init__(self, google_sdk_folder):
+ """GoogleSDKBins initialize.
+
+ Args:
+ google_sdk_folder: String, google sdk path.
+ """
+ self.gcloud_command_path = os.path.join(google_sdk_folder, "gcloud")
+ self.gsutil_command_path = os.path.join(google_sdk_folder, "gsutil")
+
+ def RunGcloud(self, cmd, **kwargs):
+ """Run gcloud command.
+
+ Args:
+ cmd: String list, command strings.
+ Ex: [config], then this function call "gcloud config".
+ **kwargs: dictionary of keyword based args to pass to func.
+
+ Returns:
+ String, return message after execute gcloud command.
+ """
+ return subprocess.check_output([self.gcloud_command_path] + cmd, **kwargs)
+
+ def RunGsutil(self, cmd, **kwargs):
+ """Run gsutil command.
+
+ Args:
+ cmd : String list, command strings.
+ Ex: [list], then this function call "gsutil list".
+ **kwargs: dictionary of keyword based args to pass to func.
+
+ Returns:
+ String, return message after execute gsutil command.
+ """
+ return subprocess.check_output([self.gsutil_command_path] + cmd, **kwargs)
+
+
+class GcpTaskRunner(base_task_runner.BaseTaskRunner):
+ """Runner to setup google cloud user information."""
+
+ WELCOME_MESSAGE_TITLE = "Setup google cloud user information"
+ WELCOME_MESSAGE = (
+ "This step will walk you through gcloud SDK installation."
+ "Then configure gcloud user information."
+ "Finally enable some gcloud API services.")
+
+ def __init__(self, config_path):
+ """Initialize parameters.
+
+ Load config file to get current values.
+
+ Args:
+ config_path: String, acloud config path.
+ """
+ config_mgr = config.AcloudConfigManager(config_path)
+ cfg = config_mgr.Load()
+ self.config_path = config_mgr.user_config_path
+ self.client_id = cfg.client_id
+ self.client_secret = cfg.client_secret
+ self.project = cfg.project
+ self.zone = cfg.zone
+ self.storage_bucket_name = cfg.storage_bucket_name
+ self.ssh_private_key_path = cfg.ssh_private_key_path
+ self.ssh_public_key_path = cfg.ssh_public_key_path
+
+ # Write default stable_host_image_name with dummy value.
+ # TODO(113091773): An additional step to create the host image.
+ if not cfg.stable_host_image_name:
+ UpdateConfigFile(self.config_path, "stable_host_image_name", "")
+
+ def _Run(self):
+ """Run GCP setup task."""
+ self._SetupGcloudInfo()
+ SetupSSHKeys(self.config_path, self.ssh_private_key_path,
+ self.ssh_public_key_path)
+
+ def _SetupGcloudInfo(self):
+ """Setup Gcloud user information.
+ 1. Setup Gcloud SDK tools.
+ 2. Setup Gcloud project.
+ a. Setup Gcloud project and zone.
+ b. Setup Client ID and Client secret.
+ c. Setup Google Cloud Storage bucket.
+ 3. Enable Gcloud API services.
+ """
+ google_sdk_init = google_sdk.GoogleSDK()
+ try:
+ google_sdk_runner = GoogleSDKBins(google_sdk_init.GetSDKBinPath())
+ self._SetupProject(google_sdk_runner)
+ self._EnableGcloudServices(google_sdk_runner)
+ finally:
+ google_sdk_init.CleanUp()
+
+ def _NeedProjectSetup(self):
+ """Confirm project setup should run or not.
+
+ If the project settings (project name and zone) are blank (either one),
+ we'll run the project setup flow. If they are set, we'll check with
+ the user if they want to update them.
+
+ Returns:
+ Boolean: True if we need to setup the project, False otherwise.
+ """
+ user_question = (
+ "Your default Project/Zone settings are:\n"
+ "project:[%s]\n"
+ "zone:[%s]\n"
+ "Would you like to update them? [y/n]\n") % (self.project, self.zone)
+
+ if not self.project or not self.zone:
+ logger.info("Project or zone is empty. Start to run setup process.")
+ return True
+ return utils.GetUserAnswerYes(user_question)
+
+ def _NeedClientIDSetup(self, project_changed):
+ """Confirm client setup should run or not.
+
+ If project changed, client ID must also have to change.
+ So tool will force to run setup function.
+ If client ID or client secret is empty, tool force to run setup function.
+ If project didn't change and config hold user client ID/secret, tool
+ would skip client ID setup.
+
+ Args:
+ project_changed: Boolean, True for project changed.
+
+ Returns:
+ Boolean: True for run setup function.
+ """
+ if project_changed:
+ logger.info("Your project changed. Start to run setup process.")
+ return True
+ elif not self.client_id or not self.client_secret:
+ logger.info("Client ID or client secret is empty. Start to run setup process.")
+ return True
+ logger.info("Project was unchanged and client ID didn't need to changed.")
+ return False
+
+ def _SetupProject(self, gcloud_runner):
+ """Setup gcloud project information.
+
+ Setup project and zone.
+ Setup client ID and client secret.
+ Setup Google Cloud Storage bucket.
+
+ Args:
+ gcloud_runner: A GcloudRunner class to run "gcloud" command.
+ """
+ project_changed = False
+ if self._NeedProjectSetup():
+ project_changed = self._UpdateProject(gcloud_runner)
+ if self._NeedClientIDSetup(project_changed):
+ self._SetupClientIDSecret()
+ self._SetupStorageBucket(gcloud_runner)
+
+ def _UpdateProject(self, gcloud_runner):
+ """Setup gcloud project name and zone name and check project changed.
+
+ Run "gcloud init" to handle gcloud project setup.
+ Then "gcloud list" to get user settings information include "project" & "zone".
+ Record project_changed for next setup steps.
+
+ Args:
+ gcloud_runner: A GcloudRunner class to run "gcloud" command.
+
+ Returns:
+ project_changed: True for project settings changed.
+ """
+ project_changed = False
+ gcloud_runner.RunGcloud(["init"])
+ gcp_config_list_out = gcloud_runner.RunGcloud(["config", "list"])
+ for line in gcp_config_list_out.splitlines():
+ project_match = _PROJECT_RE.match(line)
+ if project_match:
+ project = project_match.group("project")
+ project_changed = (self.project != project)
+ self.project = project
+ continue
+ zone_match = _ZONE_RE.match(line)
+ if zone_match:
+ self.zone = zone_match.group("zone")
+ continue
+ UpdateConfigFile(self.config_path, "project", self.project)
+ UpdateConfigFile(self.config_path, "zone", self.zone)
+ return project_changed
+
+ def _SetupClientIDSecret(self):
+ """Setup Client ID / Client Secret in config file.
+
+ User can use input new values for Client ID and Client Secret.
+ """
+ print("Please generate a new client ID/secret by following the instructions here:")
+ print("https://support.google.com/cloud/answer/6158849?hl=en")
+ # TODO: Create markdown readme instructions since the link isn't too helpful.
+ self.client_id = None
+ self.client_secret = None
+ while _InputIsEmpty(self.client_id):
+ self.client_id = str(raw_input("Enter Client ID: ").strip())
+ while _InputIsEmpty(self.client_secret):
+ self.client_secret = str(raw_input("Enter Client Secret: ").strip())
+ UpdateConfigFile(self.config_path, "client_id", self.client_id)
+ UpdateConfigFile(self.config_path, "client_secret", self.client_secret)
+
+ def _SetupStorageBucket(self, gcloud_runner):
+ """Setup storage_bucket_name in config file.
+
+ We handle the following cases:
+ 1. Bucket set in the config && bucket is valid.
+ - Configure the bucket.
+ 2. Bucket set in the config && bucket is invalid.
+ - Create a default acloud bucket and configure it
+ 3. Bucket is not set in the config.
+ - Create a default acloud bucket and configure it.
+
+ Args:
+ gcloud_runner: A GcloudRunner class to run "gsutil" command.
+ """
+ if (not self.storage_bucket_name
+ or not self._BucketIsValid(self.storage_bucket_name, gcloud_runner)):
+ self.storage_bucket_name = self._CreateDefaultBucket(gcloud_runner)
+ self._ConfigureBucket(gcloud_runner)
+ UpdateConfigFile(self.config_path, "storage_bucket_name",
+ self.storage_bucket_name)
+ logger.info("Storage bucket name set to [%s]", self.storage_bucket_name)
+
+ def _ConfigureBucket(self, gcloud_runner):
+ """Setup write access right for Android Build service account.
+
+ To avoid confuse user, we don't show messages for processing messages.
+ e.g. "No changes to gs://acloud-bucket/"
+
+ Args:
+ gcloud_runner: A GcloudRunner class to run "gsutil" command.
+ """
+ gcloud_runner.RunGsutil([
+ "acl", "ch", "-u",
+ "%s:W" % (_BUILD_SERVICE_ACCOUNT),
+ "%s" % (_BUCKET_HEADER + self.storage_bucket_name)
+ ], stderr=subprocess.STDOUT)
+
+ def _BucketIsValid(self, bucket_name, gcloud_runner):
+ """Check bucket is valid or not.
+
+ If bucket exists and region is in default region,
+ then this bucket is valid.
+
+ Args:
+ bucket_name: String, name of storage bucket.
+ gcloud_runner: A GcloudRunner class to run "gsutil" command.
+
+ Returns:
+ Boolean: True if bucket is valid, otherwise False.
+ """
+ return (self._BucketExists(bucket_name, gcloud_runner) and
+ self._BucketInDefaultRegion(bucket_name, gcloud_runner))
+
+ def _CreateDefaultBucket(self, gcloud_runner):
+ """Setup bucket to default bucket name.
+
+ Default bucket name is "acloud-{project}".
+ If default bucket exist and its region is not "US",
+ then default bucket name is changed as "acloud-{project}-us"
+ If default bucket didn't exist, tool will create it.
+
+ Args:
+ gcloud_runner: A GcloudRunner class to run "gsutil" command.
+
+ Returns:
+ String: string of bucket name.
+ """
+ bucket_name = "%s-%s" % (_DEFAULT_BUCKET_HEADER, self.project)
+ if (self._BucketExists(bucket_name, gcloud_runner) and
+ not self._BucketInDefaultRegion(bucket_name, gcloud_runner)):
+ bucket_name += ("-" + _DEFAULT_BUCKET_REGION.lower())
+ if not self._BucketExists(bucket_name, gcloud_runner):
+ self._CreateBucket(bucket_name, gcloud_runner)
+ return bucket_name
+
+ @staticmethod
+ def _BucketExists(bucket_name, gcloud_runner):
+ """Confirm bucket exist in project or not.
+
+ Args:
+ bucket_name: String, name of storage bucket.
+ gcloud_runner: A GcloudRunner class to run "gsutil" command.
+
+ Returns:
+ Boolean: True for bucket exist in project.
+ """
+ output = gcloud_runner.RunGsutil(["list"])
+ for output_line in output.splitlines():
+ match = _BUCKET_RE.match(output_line)
+ if match.group("bucket") == bucket_name:
+ return True
+ return False
+
+ @staticmethod
+ def _BucketInDefaultRegion(bucket_name, gcloud_runner):
+ """Confirm bucket region settings is "US" or not.
+
+ Args:
+ bucket_name: String, name of storage bucket.
+ gcloud_runner: A GcloudRunner class to run "gsutil" command.
+
+ Returns:
+ Boolean: True for bucket region is in default region.
+
+ Raises:
+ errors.SetupError: For parsing bucket region information error.
+ """
+ output = gcloud_runner.RunGsutil(
+ ["ls", "-L", "-b", "%s" % (_BUCKET_HEADER + bucket_name)])
+ for region_line in output.splitlines():
+ region_match = _BUCKET_REGION_RE.match(region_line.strip())
+ if region_match:
+ region = region_match.group("region").strip()
+ logger.info("Bucket[%s] is in %s (checking for %s)", bucket_name,
+ region, _DEFAULT_BUCKET_REGION)
+ if region == _DEFAULT_BUCKET_REGION:
+ return True
+ return False
+ raise errors.ParseBucketRegionError("Could not determine bucket region.")
+
+ @staticmethod
+ def _CreateBucket(bucket_name, gcloud_runner):
+ """Create new storage bucket in project.
+
+ Args:
+ bucket_name: String, name of storage bucket.
+ gcloud_runner: A GcloudRunner class to run "gsutil" command.
+ """
+ gcloud_runner.RunGsutil(["mb", "%s" % (_BUCKET_HEADER + bucket_name)])
+ logger.info("Create bucket [%s].", bucket_name)
+
+ @staticmethod
+ def _EnableGcloudServices(gcloud_runner):
+ """Enable 3 Gcloud API services.
+
+ 1. Android build service
+ 2. Compute engine service
+ 3. Google cloud storage service
+ To avoid confuse user, we don't show messages for services processing
+ messages. e.g. "Waiting for async operation operations ...."
+
+ Args:
+ gcloud_runner: A GcloudRunner class to run "gcloud" command.
+ """
+ for service in _GOOGLE_APIS:
+ gcloud_runner.RunGcloud(["services", "enable", service],
+ stderr=subprocess.STDOUT)
diff --git a/setup/gcp_setup_runner_test.py b/setup/gcp_setup_runner_test.py
new file mode 100644
index 0000000..f76f8b7
--- /dev/null
+++ b/setup/gcp_setup_runner_test.py
@@ -0,0 +1,156 @@
+#!/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.
+"""Tests for acloud.setup.gcp_setup_runner."""
+
+import unittest
+import os
+import mock
+
+# pylint: disable=no-name-in-module,import-error
+from acloud.internal.proto import user_config_pb2
+from acloud.public import config
+from acloud.setup import gcp_setup_runner
+
+_GCP_USER_CONFIG = """
+[compute]
+region = new_region
+zone = new_zone
+[core]
+account = new@google.com
+disable_usage_reporting = False
+project = new_project
+"""
+
+
+def _CreateCfgFile():
+ """A helper method that creates a mock configuration object."""
+ default_cfg = """
+project: "fake_project"
+zone: "fake_zone"
+storage_bucket_name: "fake_bucket"
+client_id: "fake_client_id"
+client_secret: "fake_client_secret"
+"""
+ return default_cfg
+
+
+# pylint: disable=protected-access
+class AcloudGCPSetupTest(unittest.TestCase):
+ """Test GCP Setup steps."""
+
+ def setUp(self):
+ """Create config and gcp_env_runner."""
+ self.cfg_path = "acloud_unittest.config"
+ file_write = open(self.cfg_path, 'w')
+ file_write.write(_CreateCfgFile().strip())
+ file_write.close()
+ self.gcp_env_runner = gcp_setup_runner.GcpTaskRunner(self.cfg_path)
+ self.gcloud_runner = gcp_setup_runner.GoogleSDKBins("")
+
+ def tearDown(self):
+ """Remove temp file."""
+ if os.path.isfile(self.cfg_path):
+ os.remove(self.cfg_path)
+
+ def testUpdateConfigFile(self):
+ """Test update config file."""
+ # Test update project field.
+ gcp_setup_runner.UpdateConfigFile(self.cfg_path, "project",
+ "test_project")
+ cfg = config.AcloudConfigManager.LoadConfigFromProtocolBuffer(
+ open(self.cfg_path, "r"), user_config_pb2.UserConfig)
+ self.assertEqual(cfg.project, "test_project")
+ self.assertEqual(cfg.ssh_private_key_path, "")
+ # Test add ssh key path in config
+ gcp_setup_runner.UpdateConfigFile(self.cfg_path,
+ "ssh_private_key_path", "test_path")
+ cfg = config.AcloudConfigManager.LoadConfigFromProtocolBuffer(
+ open(self.cfg_path, "r"), user_config_pb2.UserConfig)
+ self.assertEqual(cfg.project, "test_project")
+ self.assertEqual(cfg.ssh_private_key_path, "test_path")
+
+ @mock.patch.object(gcp_setup_runner.GcpTaskRunner, "_CreateBucket")
+ @mock.patch.object(gcp_setup_runner.GcpTaskRunner, "_BucketExists")
+ @mock.patch.object(gcp_setup_runner.GcpTaskRunner, "_BucketInDefaultRegion")
+ def testCreateDefaultBucket(self, mock_valid, mock_exist, mock_create):
+ """Test default bucket name.
+
+ Default bucket name is "acloud-{project}".
+ If default bucket exist but region is not in default region,
+ bucket name changes to "acloud-{project}-us".
+ """
+ self.gcp_env_runner.project = "fake_project"
+ mock_exist.return_value = False
+ mock_valid.return_value = False
+ mock_create.return_value = True
+ self.assertEqual(
+ "acloud-fake_project",
+ self.gcp_env_runner._CreateDefaultBucket(self.gcloud_runner))
+ mock_exist.return_value = True
+ mock_valid.return_value = False
+ self.assertEqual(
+ "acloud-fake_project-%s" %
+ gcp_setup_runner._DEFAULT_BUCKET_REGION.lower(),
+ self.gcp_env_runner._CreateDefaultBucket(self.gcloud_runner))
+
+ @mock.patch("os.path.dirname", return_value="")
+ @mock.patch("subprocess.check_output")
+ def testSeupProjectZone(self, mock_runner, mock_path):
+ """Test setup project and zone."""
+ gcloud_runner = gcp_setup_runner.GoogleSDKBins(mock_path)
+ self.gcp_env_runner.project = "fake_project"
+ self.gcp_env_runner.zone = "fake_zone"
+ mock_runner.side_effect = [0, _GCP_USER_CONFIG]
+ self.gcp_env_runner._UpdateProject(gcloud_runner)
+ self.assertEqual(self.gcp_env_runner.project, "new_project")
+ self.assertEqual(self.gcp_env_runner.zone, "new_zone")
+
+ @mock.patch("__builtin__.raw_input")
+ def testSetupClientIDSecret(self, mock_id):
+ """Test setup client ID and client secret."""
+ self.gcp_env_runner.client_id = "fake_client_id"
+ self.gcp_env_runner.client_secret = "fake_client_secret"
+ mock_id.side_effect = ["new_id", "new_secret"]
+ self.gcp_env_runner._SetupClientIDSecret()
+ self.assertEqual(self.gcp_env_runner.client_id, "new_id")
+ self.assertEqual(self.gcp_env_runner.client_secret, "new_secret")
+
+ @mock.patch.object(gcp_setup_runner.GoogleSDKBins, "RunGsutil")
+ def testBucketExists(self, mock_bucket_name):
+ """Test bucket name exist or not."""
+ mock_bucket_name.return_value = "gs://acloud-fake_project/"
+ self.assertTrue(
+ self.gcp_env_runner._BucketExists("acloud-fake_project",
+ self.gcloud_runner))
+ self.assertFalse(
+ self.gcp_env_runner._BucketExists("wrong_project",
+ self.gcloud_runner))
+
+ @mock.patch.object(gcp_setup_runner.GoogleSDKBins, "RunGsutil")
+ def testBucketNotInDefaultRegion(self, mock_region):
+ """Test bucket region is in default region or not."""
+ mock_region.return_value = "Location constraint:ASIA"
+ self.assertFalse(
+ self.gcp_env_runner._BucketInDefaultRegion("test-bucket",
+ self.gcloud_runner))
+ mock_region.return_value = "Location constraint:US"
+ self.assertTrue(
+ self.gcp_env_runner._BucketInDefaultRegion("test-bucket",
+ self.gcloud_runner))
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/setup/setup.py b/setup/setup.py
index 93d3c3d..aaa272c 100644
--- a/setup/setup.py
+++ b/setup/setup.py
@@ -23,11 +23,17 @@
from acloud.internal.lib import utils
from acloud.setup import host_setup_runner
+from acloud.setup import gcp_setup_runner
-def Run():
+def Run(args):
"""Run setup.
+ Setup options:
+ -host: Setup host settings.
+ -gcp_init: Setup gcp settings.
+ -None, default behavior will setup host and gcp settings.
+
Args:
args: Namespace object from argparse.parse_args.
"""
@@ -36,8 +42,15 @@
_PrintWelcomeMessage()
# 2.Init all subtasks in queue and traverse them.
- task_queue = [host_setup_runner.CuttlefishPkgInstaller(),
- host_setup_runner.CuttlefishHostSetup(),]
+ host_runner = host_setup_runner.CuttlefishPkgInstaller()
+ host_env_runner = host_setup_runner.CuttlefishHostSetup()
+ gcp_runner = gcp_setup_runner.GcpTaskRunner(args.config_file)
+ task_queue = []
+ if args.host or not args.gcp_init:
+ task_queue.append(host_runner)
+ task_queue.append(host_env_runner)
+ if args.gcp_init or not args.host:
+ task_queue.append(gcp_runner)
for subtask in task_queue:
subtask.Run()
@@ -63,4 +76,6 @@
def _PrintUsage():
"""Print cmd usage hints when acloud setup been finished."""
- utils.PrintColorString("\nIf you'd like more info, run '#acloud create --help'")
+ utils.PrintColorString("")
+ utils.PrintColorString("Setup process finished")
+ utils.PrintColorString("To get started creating AVDs, run '#acloud create'")
diff --git a/setup/setup_args.py b/setup/setup_args.py
index c82ed57..9c1f5d0 100644
--- a/setup/setup_args.py
+++ b/setup/setup_args.py
@@ -39,4 +39,11 @@
dest="host",
required=False,
help="Setup host to run local instance of an Android Virtual Device.")
+ setup_parser.add_argument(
+ "--gcp_init",
+ action="store_true",
+ dest="gcp_init",
+ required=False,
+ help="Setup Google Cloud project name and enable required GCP APIs."
+ "Ex: Google Cloud Storage/ Internal Android Build/ Compute Engine")
return setup_parser