blob: e7eb8e145e9add1fc362a9a2ad50daeac24436dc [file] [log] [blame]
Keun Soo Yimb293fdb2016-09-21 16:03:44 -07001#!/usr/bin/env python
2#
3# Copyright 2016 - The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17"""Config manager.
18
19Three protobuf messages are defined in
20 driver/internal/config/proto/internal_config.proto
21 driver/internal/config/proto/user_config.proto
22
23Internal config file User config file
24 | |
25 v v
26 InternalConfig UserConfig
27 (proto message) (proto message)
28 | |
29 | |
30 |-> AcloudConfig <-|
31
32At runtime, AcloudConfigManager performs the following steps.
33- Load driver config file into a InternalConfig message instance.
34- Load user config file into a UserConfig message instance.
35- Create AcloudConfig using InternalConfig and UserConfig.
36
Kevin Chengb5963882018-05-09 00:06:27 -070037TODO:
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070038 1. Add support for override configs with command line args.
39 2. Scan all configs to find the right config for given branch and build_id.
40 Raise an error if the given build_id is smaller than min_build_id
41 only applies to release build id.
42 Raise an error if the branch is not supported.
43
44"""
45
46import logging
47import os
48
chojoyce35556da2019-10-25 16:02:36 +080049import six
50
Sam Chiu46ea3112018-05-18 10:47:52 +080051from google.protobuf import text_format
52
herbertxue18c9b262018-07-12 19:14:49 +080053# pylint: disable=no-name-in-module,import-error
Sam Chiu7de3b232018-12-06 19:45:52 +080054from acloud import errors
herbertxuefd15dfd2018-12-04 11:26:27 +080055from acloud.internal import constants
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070056from acloud.internal.proto import internal_config_pb2
57from acloud.internal.proto import user_config_pb2
Sam Chiu58dad6e2018-08-27 19:50:33 +080058from acloud.create import create_args
Sam Chiu46ea3112018-05-18 10:47:52 +080059
herbertxue1512f8a2019-06-27 13:56:23 +080060
61logger = logging.getLogger(__name__)
62
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070063_CONFIG_DATA_PATH = os.path.join(
64 os.path.dirname(os.path.abspath(__file__)), "data")
herbertxue18c9b262018-07-12 19:14:49 +080065_DEFAULT_CONFIG_FILE = "acloud.config"
herbertxue36b99f12021-03-25 14:47:28 +080066_DEFAULT_HW_PROPERTY = "cpu:4,resolution:720x1280,dpi:320,memory:4g"
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070067
cylan9af14692020-02-21 18:11:35 +080068# VERSION
69_VERSION_FILE = "VERSION"
70_UNKNOWN = "UNKNOWN"
Sam Chiu4f398b72020-03-04 20:43:04 +080071_NUM_INSTANCES_ARG = "-num_instances"
cylan9af14692020-02-21 18:11:35 +080072
73
74def GetVersion():
75 """Print the version of acloud.
76
77 The VERSION file is built into the acloud binary. The version file path is
78 under "public/data".
79
80 Returns:
81 String of the acloud version.
82 """
83 version_file_path = os.path.join(_CONFIG_DATA_PATH, _VERSION_FILE)
84 if os.path.exists(version_file_path):
85 with open(version_file_path) as version_file:
86 return version_file.read()
87 return _UNKNOWN
88
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070089
herbertxue18c9b262018-07-12 19:14:49 +080090def GetDefaultConfigFile():
Kevin Cheng99cf3d32018-10-05 02:09:21 -070091 """Return path to default config file."""
92 config_path = os.path.join(os.path.expanduser("~"), ".config", "acloud")
93 # Create the default config dir if it doesn't exist.
94 if not os.path.exists(config_path):
95 os.makedirs(config_path)
herbertxue18c9b262018-07-12 19:14:49 +080096 return os.path.join(config_path, _DEFAULT_CONFIG_FILE)
97
98
Sam Chiuc64f3432018-08-17 11:19:06 +080099def GetAcloudConfig(args):
100 """Helper function to initialize Config object.
101
102 Args:
103 args: Namespace object from argparse.parse_args.
104
105 Return:
106 An instance of AcloudConfig.
107 """
108 config_mgr = AcloudConfigManager(args.config_file)
109 cfg = config_mgr.Load()
110 cfg.OverrideWithArgs(args)
111 return cfg
112
113
herbertxue36b99f12021-03-25 14:47:28 +0800114class AcloudConfig():
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700115 """A class that holds all configurations for acloud."""
116
117 REQUIRED_FIELD = [
herbertxue18c9b262018-07-12 19:14:49 +0800118 "machine_type", "network", "min_machine_size",
119 "disk_image_name", "disk_image_mime_type"
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700120 ]
121
Kevin Chengf13e8be2019-05-10 14:17:32 -0700122 # pylint: disable=too-many-statements
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700123 def __init__(self, usr_cfg, internal_cfg):
124 """Initialize.
125
126 Args:
127 usr_cfg: A protobuf object that holds the user configurations.
128 internal_cfg: A protobuf object that holds internal configurations.
129 """
130 self.service_account_name = usr_cfg.service_account_name
Sam Chiu46ea3112018-05-18 10:47:52 +0800131 # pylint: disable=invalid-name
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700132 self.service_account_private_key_path = (
133 usr_cfg.service_account_private_key_path)
xingdai8a00d462018-07-30 14:24:48 -0700134 self.service_account_json_private_key_path = (
135 usr_cfg.service_account_json_private_key_path)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700136 self.creds_cache_file = internal_cfg.creds_cache_file
137 self.user_agent = internal_cfg.user_agent
138 self.client_id = usr_cfg.client_id
139 self.client_secret = usr_cfg.client_secret
140
141 self.project = usr_cfg.project
142 self.zone = usr_cfg.zone
143 self.machine_type = (usr_cfg.machine_type or
144 internal_cfg.default_usr_cfg.machine_type)
Sam Chiu46ea3112018-05-18 10:47:52 +0800145 self.network = usr_cfg.network or internal_cfg.default_usr_cfg.network
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700146 self.ssh_private_key_path = usr_cfg.ssh_private_key_path
Fang Dengfed6a6f2017-03-01 18:27:28 -0800147 self.ssh_public_key_path = usr_cfg.ssh_public_key_path
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700148 self.storage_bucket_name = usr_cfg.storage_bucket_name
149 self.metadata_variable = {
Sam Chiu46ea3112018-05-18 10:47:52 +0800150 key: val for key, val in
chojoyce35556da2019-10-25 16:02:36 +0800151 six.iteritems(internal_cfg.default_usr_cfg.metadata_variable)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700152 }
153 self.metadata_variable.update(usr_cfg.metadata_variable)
154
155 self.device_resolution_map = {
Sam Chiu46ea3112018-05-18 10:47:52 +0800156 device: resolution for device, resolution in
chojoyce35556da2019-10-25 16:02:36 +0800157 six.iteritems(internal_cfg.device_resolution_map)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700158 }
159 self.device_default_orientation_map = {
Sam Chiu46ea3112018-05-18 10:47:52 +0800160 device: orientation for device, orientation in
chojoyce35556da2019-10-25 16:02:36 +0800161 six.iteritems(internal_cfg.device_default_orientation_map)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700162 }
Fang Dengcef4b112017-03-02 11:20:17 -0800163 self.no_project_access_msg_map = {
Sam Chiu46ea3112018-05-18 10:47:52 +0800164 project: msg for project, msg in
chojoyce35556da2019-10-25 16:02:36 +0800165 six.iteritems(internal_cfg.no_project_access_msg_map)
Sam Chiu46ea3112018-05-18 10:47:52 +0800166 }
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700167 self.min_machine_size = internal_cfg.min_machine_size
168 self.disk_image_name = internal_cfg.disk_image_name
169 self.disk_image_mime_type = internal_cfg.disk_image_mime_type
170 self.disk_image_extension = internal_cfg.disk_image_extension
171 self.disk_raw_image_name = internal_cfg.disk_raw_image_name
172 self.disk_raw_image_extension = internal_cfg.disk_raw_image_extension
173 self.valid_branch_and_min_build_id = {
Sam Chiu46ea3112018-05-18 10:47:52 +0800174 branch: min_build_id for branch, min_build_id in
chojoyce35556da2019-10-25 16:02:36 +0800175 six.iteritems(internal_cfg.valid_branch_and_min_build_id)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700176 }
177 self.precreated_data_image_map = {
Sam Chiu46ea3112018-05-18 10:47:52 +0800178 size_gb: image_name for size_gb, image_name in
chojoyce35556da2019-10-25 16:02:36 +0800179 six.iteritems(internal_cfg.precreated_data_image)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700180 }
181 self.extra_data_disk_size_gb = (
182 usr_cfg.extra_data_disk_size_gb or
183 internal_cfg.default_usr_cfg.extra_data_disk_size_gb)
184 if self.extra_data_disk_size_gb > 0:
185 if "cfg_sta_persistent_data_device" not in usr_cfg.metadata_variable:
186 # If user did not set it explicity, use default.
187 self.metadata_variable["cfg_sta_persistent_data_device"] = (
188 internal_cfg.default_extra_data_disk_device)
189 if "cfg_sta_ephemeral_data_size_mb" in usr_cfg.metadata_variable:
190 raise errors.ConfigError(
191 "The following settings can't be set at the same time: "
192 "extra_data_disk_size_gb and"
193 "metadata variable cfg_sta_ephemeral_data_size_mb.")
194 if "cfg_sta_ephemeral_data_size_mb" in self.metadata_variable:
195 del self.metadata_variable["cfg_sta_ephemeral_data_size_mb"]
196
Kevin Chengc330f6f2019-05-13 09:32:42 -0700197 # Additional scopes to be passed to the created instance
198 self.extra_scopes = usr_cfg.extra_scopes
199
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700200 # Fields that can be overriden by args
201 self.orientation = usr_cfg.orientation
202 self.resolution = usr_cfg.resolution
203
herbertxue41322542020-04-20 16:07:48 +0800204 self.stable_host_image_family = usr_cfg.stable_host_image_family
Kevin Chengb5963882018-05-09 00:06:27 -0700205 self.stable_host_image_name = (
206 usr_cfg.stable_host_image_name or
207 internal_cfg.default_usr_cfg.stable_host_image_name)
208 self.stable_host_image_project = (
209 usr_cfg.stable_host_image_project or
210 internal_cfg.default_usr_cfg.stable_host_image_project)
211 self.kernel_build_target = internal_cfg.kernel_build_target
212
213 self.emulator_build_target = internal_cfg.emulator_build_target
214 self.stable_goldfish_host_image_name = (
215 usr_cfg.stable_goldfish_host_image_name or
216 internal_cfg.default_usr_cfg.stable_goldfish_host_image_name)
217 self.stable_goldfish_host_image_project = (
218 usr_cfg.stable_goldfish_host_image_project or
219 internal_cfg.default_usr_cfg.stable_goldfish_host_image_project)
220
Richard Fung97503b22019-02-06 13:43:38 -0800221 self.stable_cheeps_host_image_name = (
222 usr_cfg.stable_cheeps_host_image_name or
223 internal_cfg.default_usr_cfg.stable_cheeps_host_image_name)
224 self.stable_cheeps_host_image_project = (
225 usr_cfg.stable_cheeps_host_image_project or
226 internal_cfg.default_usr_cfg.stable_cheeps_host_image_project)
Shao-Chuan Lee26005262020-09-10 21:51:33 +0900227 self.betty_image = usr_cfg.betty_image
Richard Fung97503b22019-02-06 13:43:38 -0800228
cyland370db22019-07-17 16:04:00 +0800229 self.extra_args_ssh_tunnel = usr_cfg.extra_args_ssh_tunnel
230
Sam Chiu58dad6e2018-08-27 19:50:33 +0800231 self.common_hw_property_map = internal_cfg.common_hw_property_map
232 self.hw_property = usr_cfg.hw_property
233
Kevin Chengd3083bf2019-05-08 15:50:57 -0700234 self.launch_args = usr_cfg.launch_args
herbertxueb1574472021-04-08 14:44:47 +0800235 self.api_key = usr_cfg.api_key
236 self.api_url = usr_cfg.api_url
Kevin Chengf13e8be2019-05-10 14:17:32 -0700237 self.instance_name_pattern = (
238 usr_cfg.instance_name_pattern or
239 internal_cfg.default_usr_cfg.instance_name_pattern)
Cody Schuffelen102b3b52019-07-17 10:26:35 -0700240 self.fetch_cvd_version = (
241 usr_cfg.fetch_cvd_version or
242 internal_cfg.default_usr_cfg.fetch_cvd_version)
243 if usr_cfg.HasField("enable_multi_stage") is not None:
244 self.enable_multi_stage = usr_cfg.enable_multi_stage
245 elif internal_cfg.default_usr_cfg.HasField("enable_multi_stage"):
246 self.enable_multi_stage = internal_cfg.default_usr_cfg.enable_multi_stage
247 else:
248 self.enable_multi_stage = False
Kevin Chengd3083bf2019-05-08 15:50:57 -0700249
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700250 # Verify validity of configurations.
251 self.Verify()
252
herbertxueffbcbc22020-07-07 14:35:34 +0800253 # pylint: disable=too-many-branches
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700254 def OverrideWithArgs(self, parsed_args):
255 """Override configuration values with args passed in from cmd line.
256
257 Args:
258 parsed_args: Args parsed from command line.
259 """
Sam Chiu58dad6e2018-08-27 19:50:33 +0800260 if parsed_args.which == create_args.CMD_CREATE and parsed_args.spec:
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700261 if not self.resolution:
262 self.resolution = self.device_resolution_map.get(
263 parsed_args.spec, "")
264 if not self.orientation:
265 self.orientation = self.device_default_orientation_map.get(
266 parsed_args.spec, "")
267 if parsed_args.email:
268 self.service_account_name = parsed_args.email
xingdai8a00d462018-07-30 14:24:48 -0700269 if parsed_args.service_account_json_private_key_path:
270 self.service_account_json_private_key_path = (
271 parsed_args.service_account_json_private_key_path)
Kevin Chengbced4af2018-06-26 10:35:01 -0700272 if parsed_args.which == "create_gf" and parsed_args.base_image:
273 self.stable_goldfish_host_image_name = parsed_args.base_image
Kevin Chengc9424a82018-10-25 11:34:55 -0700274 if parsed_args.which in [create_args.CMD_CREATE, "create_cf"]:
275 if parsed_args.network:
276 self.network = parsed_args.network
Cody Schuffelen102b3b52019-07-17 10:26:35 -0700277 if parsed_args.multi_stage_launch is not None:
278 self.enable_multi_stage = parsed_args.multi_stage_launch
herbertxueffbcbc22020-07-07 14:35:34 +0800279 if parsed_args.which in [create_args.CMD_CREATE, "create_cf", "create_gf"]:
280 if parsed_args.zone:
281 self.zone = parsed_args.zone
Sam Chiu4f398b72020-03-04 20:43:04 +0800282 if (parsed_args.which == "create_cf" and
283 parsed_args.num_avds_per_instance > 1):
284 scrubbed_args = [arg for arg in self.launch_args.split()
285 if _NUM_INSTANCES_ARG not in arg]
286 scrubbed_args.append("%s=%d" % (_NUM_INSTANCES_ARG,
287 parsed_args.num_avds_per_instance))
288
289 self.launch_args = " ".join(scrubbed_args)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700290
herbertxue908bfa82020-11-24 17:49:26 +0800291 def GetDefaultHwProperty(self, flavor, instance_type=None):
292 """Get default hw configuration values.
herbertxuefd15dfd2018-12-04 11:26:27 +0800293
Sam Chiu1191e2d2020-03-13 13:22:11 +0800294 HwProperty will be overrided according to the change of flavor and
295 instance type. The format of key is flavor or instance_type-flavor.
296 e.g: 'phone' or 'local-phone'.
herbertxue908bfa82020-11-24 17:49:26 +0800297 If the giving key is not found, get hw configuration with a default
Sam Chiu1191e2d2020-03-13 13:22:11 +0800298 phone property.
herbertxuefd15dfd2018-12-04 11:26:27 +0800299
300 Args:
Sam Chiu1191e2d2020-03-13 13:22:11 +0800301 flavor: String of flavor name.
302 instance_type: String of instance type.
herbertxue908bfa82020-11-24 17:49:26 +0800303
304 Returns:
305 String of device hardware property, it would be like
306 "cpu:4,resolution:720x1280,dpi:320,memory:4g".
herbertxuefd15dfd2018-12-04 11:26:27 +0800307 """
Sam Chiu1191e2d2020-03-13 13:22:11 +0800308 hw_key = ("%s-%s" % (instance_type, flavor)
309 if instance_type == constants.INSTANCE_TYPE_LOCAL else flavor)
herbertxue908bfa82020-11-24 17:49:26 +0800310 return self.common_hw_property_map.get(hw_key, _DEFAULT_HW_PROPERTY)
herbertxuefd15dfd2018-12-04 11:26:27 +0800311
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700312 def Verify(self):
313 """Verify configuration fields."""
herbertxuee3c05da2021-04-27 10:37:18 +0800314 missing = self.GetMissingFields(self.REQUIRED_FIELD)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700315 if missing:
316 raise errors.ConfigError(
317 "Missing required configuration fields: %s" % missing)
Sam Chiu46ea3112018-05-18 10:47:52 +0800318 if (self.extra_data_disk_size_gb and self.extra_data_disk_size_gb not in
319 self.precreated_data_image_map):
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700320 raise errors.ConfigError(
321 "Supported extra_data_disk_size_gb options(gb): %s, "
322 "invalid value: %d" % (self.precreated_data_image_map.keys(),
323 self.extra_data_disk_size_gb))
324
herbertxuee3c05da2021-04-27 10:37:18 +0800325 def GetMissingFields(self, fields):
326 """Get missing required fields.
327
328 Args:
329 fields: List of field names.
330
331 Returns:
332 List of missing field names.
333 """
334 return [f for f in fields if not getattr(self, f)]
335
Sam Chiu43d81c52020-02-04 10:49:47 +0800336 def SupportRemoteInstance(self):
337 """Return True if gcp project is provided in config."""
herbertxue908bfa82020-11-24 17:49:26 +0800338 return bool(self.project)
Sam Chiu43d81c52020-02-04 10:49:47 +0800339
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700340
herbertxue36b99f12021-03-25 14:47:28 +0800341class AcloudConfigManager():
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700342 """A class that loads configurations."""
343
344 _DEFAULT_INTERNAL_CONFIG_PATH = os.path.join(_CONFIG_DATA_PATH,
345 "default.config")
346
347 def __init__(self,
348 user_config_path,
349 internal_config_path=_DEFAULT_INTERNAL_CONFIG_PATH):
herbertxue18c9b262018-07-12 19:14:49 +0800350 """Initialize with user specified paths to configs.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700351
352 Args:
353 user_config_path: path to the user config.
354 internal_config_path: path to the internal conifg.
355 """
herbertxue34776bb2018-07-03 21:57:48 +0800356 self.user_config_path = user_config_path
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700357 self._internal_config_path = internal_config_path
358
359 def Load(self):
herbertxue18c9b262018-07-12 19:14:49 +0800360 """Load the configurations.
361
362 Load user config with some special design.
363 1. User specified user config:
364 a.User config exist: Load config.
365 b.User config didn't exist: Raise exception.
366 2. User didn't specify user config, use default config:
367 a.Default config exist: Load config.
368 b.Default config didn't exist: provide empty usr_cfg.
369 """
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700370 internal_cfg = None
371 usr_cfg = None
372 try:
373 with open(self._internal_config_path) as config_file:
374 internal_cfg = self.LoadConfigFromProtocolBuffer(
375 config_file, internal_config_pb2.InternalConfig)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700376 except OSError as e:
Sam Chiu46ea3112018-05-18 10:47:52 +0800377 raise errors.ConfigError("Could not load config files: %s" % str(e))
herbertxue18c9b262018-07-12 19:14:49 +0800378 # Load user config file
herbertxue34776bb2018-07-03 21:57:48 +0800379 if self.user_config_path:
380 if os.path.exists(self.user_config_path):
381 with open(self.user_config_path, "r") as config_file:
herbertxue18c9b262018-07-12 19:14:49 +0800382 usr_cfg = self.LoadConfigFromProtocolBuffer(
383 config_file, user_config_pb2.UserConfig)
384 else:
385 raise errors.ConfigError("The file doesn't exist: %s" %
herbertxue34776bb2018-07-03 21:57:48 +0800386 (self.user_config_path))
herbertxue18c9b262018-07-12 19:14:49 +0800387 else:
herbertxue34776bb2018-07-03 21:57:48 +0800388 self.user_config_path = GetDefaultConfigFile()
389 if os.path.exists(self.user_config_path):
390 with open(self.user_config_path, "r") as config_file:
herbertxue18c9b262018-07-12 19:14:49 +0800391 usr_cfg = self.LoadConfigFromProtocolBuffer(
392 config_file, user_config_pb2.UserConfig)
393 else:
394 usr_cfg = user_config_pb2.UserConfig()
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700395 return AcloudConfig(usr_cfg, internal_cfg)
396
397 @staticmethod
398 def LoadConfigFromProtocolBuffer(config_file, message_type):
399 """Load config from a text-based protocol buffer file.
400
401 Args:
402 config_file: A python File object.
403 message_type: A proto message class.
404
405 Returns:
406 An instance of type "message_type" populated with data
407 from the file.
408 """
409 try:
410 config = message_type()
411 text_format.Merge(config_file.read(), config)
412 return config
413 except text_format.ParseError as e:
414 raise errors.ConfigError("Could not parse config: %s" % str(e))