blob: 7c976e2d2f57e85938e0f97e9d067bb80e408b90 [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
Sam Chiu46ea3112018-05-18 10:47:52 +080049from google.protobuf import text_format
50
herbertxue18c9b262018-07-12 19:14:49 +080051# pylint: disable=no-name-in-module,import-error
Sam Chiu7de3b232018-12-06 19:45:52 +080052from acloud import errors
herbertxuefd15dfd2018-12-04 11:26:27 +080053from acloud.internal import constants
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070054from acloud.internal.proto import internal_config_pb2
55from acloud.internal.proto import user_config_pb2
Sam Chiu58dad6e2018-08-27 19:50:33 +080056from acloud.create import create_args
Sam Chiu46ea3112018-05-18 10:47:52 +080057
herbertxue1512f8a2019-06-27 13:56:23 +080058
59logger = logging.getLogger(__name__)
60
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070061_CONFIG_DATA_PATH = os.path.join(
62 os.path.dirname(os.path.abspath(__file__)), "data")
herbertxue18c9b262018-07-12 19:14:49 +080063_DEFAULT_CONFIG_FILE = "acloud.config"
herbertxue36b99f12021-03-25 14:47:28 +080064_DEFAULT_HW_PROPERTY = "cpu:4,resolution:720x1280,dpi:320,memory:4g"
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070065
cylan9af14692020-02-21 18:11:35 +080066# VERSION
67_VERSION_FILE = "VERSION"
68_UNKNOWN = "UNKNOWN"
Sam Chiu4f398b72020-03-04 20:43:04 +080069_NUM_INSTANCES_ARG = "-num_instances"
cylan9af14692020-02-21 18:11:35 +080070
71
72def GetVersion():
73 """Print the version of acloud.
74
75 The VERSION file is built into the acloud binary. The version file path is
76 under "public/data".
77
78 Returns:
79 String of the acloud version.
80 """
81 version_file_path = os.path.join(_CONFIG_DATA_PATH, _VERSION_FILE)
82 if os.path.exists(version_file_path):
83 with open(version_file_path) as version_file:
84 return version_file.read()
85 return _UNKNOWN
86
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070087
herbertxue18c9b262018-07-12 19:14:49 +080088def GetDefaultConfigFile():
Kevin Cheng99cf3d32018-10-05 02:09:21 -070089 """Return path to default config file."""
90 config_path = os.path.join(os.path.expanduser("~"), ".config", "acloud")
91 # Create the default config dir if it doesn't exist.
92 if not os.path.exists(config_path):
93 os.makedirs(config_path)
herbertxue18c9b262018-07-12 19:14:49 +080094 return os.path.join(config_path, _DEFAULT_CONFIG_FILE)
95
96
herbertxue20bca0a2021-06-07 09:54:15 +080097def GetUserConfigPath(config_path):
98 """Get Acloud user config file path.
99
100 If there is no config provided, Acloud would use default config path.
101
102 Args:
103 config_path: String, path of Acloud config file.
104
105 Returns:
106 Path (string) of the Acloud config.
107 """
108 if config_path:
109 return config_path
110 return GetDefaultConfigFile()
111
112
Sam Chiuc64f3432018-08-17 11:19:06 +0800113def GetAcloudConfig(args):
114 """Helper function to initialize Config object.
115
116 Args:
117 args: Namespace object from argparse.parse_args.
118
119 Return:
120 An instance of AcloudConfig.
121 """
122 config_mgr = AcloudConfigManager(args.config_file)
123 cfg = config_mgr.Load()
124 cfg.OverrideWithArgs(args)
125 return cfg
126
127
herbertxue36b99f12021-03-25 14:47:28 +0800128class AcloudConfig():
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700129 """A class that holds all configurations for acloud."""
130
131 REQUIRED_FIELD = [
herbertxue18c9b262018-07-12 19:14:49 +0800132 "machine_type", "network", "min_machine_size",
133 "disk_image_name", "disk_image_mime_type"
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700134 ]
135
Kevin Chengf13e8be2019-05-10 14:17:32 -0700136 # pylint: disable=too-many-statements
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700137 def __init__(self, usr_cfg, internal_cfg):
138 """Initialize.
139
140 Args:
141 usr_cfg: A protobuf object that holds the user configurations.
142 internal_cfg: A protobuf object that holds internal configurations.
143 """
144 self.service_account_name = usr_cfg.service_account_name
Sam Chiu46ea3112018-05-18 10:47:52 +0800145 # pylint: disable=invalid-name
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700146 self.service_account_private_key_path = (
147 usr_cfg.service_account_private_key_path)
xingdai8a00d462018-07-30 14:24:48 -0700148 self.service_account_json_private_key_path = (
149 usr_cfg.service_account_json_private_key_path)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700150 self.creds_cache_file = internal_cfg.creds_cache_file
151 self.user_agent = internal_cfg.user_agent
152 self.client_id = usr_cfg.client_id
153 self.client_secret = usr_cfg.client_secret
154
155 self.project = usr_cfg.project
156 self.zone = usr_cfg.zone
157 self.machine_type = (usr_cfg.machine_type or
158 internal_cfg.default_usr_cfg.machine_type)
Sam Chiu46ea3112018-05-18 10:47:52 +0800159 self.network = usr_cfg.network or internal_cfg.default_usr_cfg.network
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700160 self.ssh_private_key_path = usr_cfg.ssh_private_key_path
Fang Dengfed6a6f2017-03-01 18:27:28 -0800161 self.ssh_public_key_path = usr_cfg.ssh_public_key_path
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700162 self.storage_bucket_name = usr_cfg.storage_bucket_name
herbertxued9809d12021-08-04 14:53:33 +0800163 self.metadata_variable = dict(
chojoyceb6478442022-03-10 16:44:55 +0800164 internal_cfg.default_usr_cfg.metadata_variable.items())
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700165 self.metadata_variable.update(usr_cfg.metadata_variable)
166
herbertxued9809d12021-08-04 14:53:33 +0800167 self.device_resolution_map = dict(
chojoyceb6478442022-03-10 16:44:55 +0800168 internal_cfg.device_resolution_map.items())
herbertxued9809d12021-08-04 14:53:33 +0800169 self.device_default_orientation_map = dict(
chojoyceb6478442022-03-10 16:44:55 +0800170 internal_cfg.device_default_orientation_map.items())
herbertxued9809d12021-08-04 14:53:33 +0800171 self.no_project_access_msg_map = dict(
chojoyceb6478442022-03-10 16:44:55 +0800172 internal_cfg.no_project_access_msg_map.items())
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700173 self.min_machine_size = internal_cfg.min_machine_size
174 self.disk_image_name = internal_cfg.disk_image_name
175 self.disk_image_mime_type = internal_cfg.disk_image_mime_type
176 self.disk_image_extension = internal_cfg.disk_image_extension
177 self.disk_raw_image_name = internal_cfg.disk_raw_image_name
178 self.disk_raw_image_extension = internal_cfg.disk_raw_image_extension
herbertxued9809d12021-08-04 14:53:33 +0800179 self.valid_branch_and_min_build_id = dict(
chojoyceb6478442022-03-10 16:44:55 +0800180 internal_cfg.valid_branch_and_min_build_id.items())
herbertxued9809d12021-08-04 14:53:33 +0800181 self.precreated_data_image_map = dict(
chojoyceb6478442022-03-10 16:44:55 +0800182 internal_cfg.precreated_data_image.items())
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700183 self.extra_data_disk_size_gb = (
184 usr_cfg.extra_data_disk_size_gb or
185 internal_cfg.default_usr_cfg.extra_data_disk_size_gb)
186 if self.extra_data_disk_size_gb > 0:
187 if "cfg_sta_persistent_data_device" not in usr_cfg.metadata_variable:
188 # If user did not set it explicity, use default.
189 self.metadata_variable["cfg_sta_persistent_data_device"] = (
190 internal_cfg.default_extra_data_disk_device)
191 if "cfg_sta_ephemeral_data_size_mb" in usr_cfg.metadata_variable:
192 raise errors.ConfigError(
193 "The following settings can't be set at the same time: "
194 "extra_data_disk_size_gb and"
195 "metadata variable cfg_sta_ephemeral_data_size_mb.")
196 if "cfg_sta_ephemeral_data_size_mb" in self.metadata_variable:
197 del self.metadata_variable["cfg_sta_ephemeral_data_size_mb"]
198
Kevin Chengc330f6f2019-05-13 09:32:42 -0700199 # Additional scopes to be passed to the created instance
200 self.extra_scopes = usr_cfg.extra_scopes
201
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700202 # Fields that can be overriden by args
203 self.orientation = usr_cfg.orientation
204 self.resolution = usr_cfg.resolution
205
herbertxue41322542020-04-20 16:07:48 +0800206 self.stable_host_image_family = usr_cfg.stable_host_image_family
Kevin Chengb5963882018-05-09 00:06:27 -0700207 self.stable_host_image_name = (
208 usr_cfg.stable_host_image_name or
209 internal_cfg.default_usr_cfg.stable_host_image_name)
210 self.stable_host_image_project = (
211 usr_cfg.stable_host_image_project or
212 internal_cfg.default_usr_cfg.stable_host_image_project)
213 self.kernel_build_target = internal_cfg.kernel_build_target
214
215 self.emulator_build_target = internal_cfg.emulator_build_target
216 self.stable_goldfish_host_image_name = (
217 usr_cfg.stable_goldfish_host_image_name or
218 internal_cfg.default_usr_cfg.stable_goldfish_host_image_name)
219 self.stable_goldfish_host_image_project = (
220 usr_cfg.stable_goldfish_host_image_project or
221 internal_cfg.default_usr_cfg.stable_goldfish_host_image_project)
222
Richard Fung97503b22019-02-06 13:43:38 -0800223 self.stable_cheeps_host_image_name = (
224 usr_cfg.stable_cheeps_host_image_name or
225 internal_cfg.default_usr_cfg.stable_cheeps_host_image_name)
226 self.stable_cheeps_host_image_project = (
227 usr_cfg.stable_cheeps_host_image_project or
228 internal_cfg.default_usr_cfg.stable_cheeps_host_image_project)
Shao-Chuan Lee26005262020-09-10 21:51:33 +0900229 self.betty_image = usr_cfg.betty_image
Richard Fung97503b22019-02-06 13:43:38 -0800230
cyland370db22019-07-17 16:04:00 +0800231 self.extra_args_ssh_tunnel = usr_cfg.extra_args_ssh_tunnel
232
Sam Chiu58dad6e2018-08-27 19:50:33 +0800233 self.common_hw_property_map = internal_cfg.common_hw_property_map
234 self.hw_property = usr_cfg.hw_property
235
Kevin Chengd3083bf2019-05-08 15:50:57 -0700236 self.launch_args = usr_cfg.launch_args
herbertxueeadbba02021-07-15 14:39:26 +0800237 self.oxygen_client = usr_cfg.oxygen_client
herbertxue10f852b2021-08-30 16:06:59 +0800238 self.oxygen_lease_args = usr_cfg.oxygen_lease_args
Kevin Chengf13e8be2019-05-10 14:17:32 -0700239 self.instance_name_pattern = (
240 usr_cfg.instance_name_pattern or
241 internal_cfg.default_usr_cfg.instance_name_pattern)
Cody Schuffelen102b3b52019-07-17 10:26:35 -0700242 self.fetch_cvd_version = (
243 usr_cfg.fetch_cvd_version or
244 internal_cfg.default_usr_cfg.fetch_cvd_version)
245 if usr_cfg.HasField("enable_multi_stage") is not None:
246 self.enable_multi_stage = usr_cfg.enable_multi_stage
247 elif internal_cfg.default_usr_cfg.HasField("enable_multi_stage"):
248 self.enable_multi_stage = internal_cfg.default_usr_cfg.enable_multi_stage
249 else:
250 self.enable_multi_stage = False
Yuchen Hec4a8f252021-10-21 18:15:29 +0000251 self.disk_type = usr_cfg.disk_type
Kevin Chengd3083bf2019-05-08 15:50:57 -0700252
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700253 # Verify validity of configurations.
254 self.Verify()
255
herbertxueffbcbc22020-07-07 14:35:34 +0800256 # pylint: disable=too-many-branches
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700257 def OverrideWithArgs(self, parsed_args):
258 """Override configuration values with args passed in from cmd line.
259
260 Args:
261 parsed_args: Args parsed from command line.
262 """
Sam Chiu58dad6e2018-08-27 19:50:33 +0800263 if parsed_args.which == create_args.CMD_CREATE and parsed_args.spec:
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700264 if not self.resolution:
265 self.resolution = self.device_resolution_map.get(
266 parsed_args.spec, "")
267 if not self.orientation:
268 self.orientation = self.device_default_orientation_map.get(
269 parsed_args.spec, "")
270 if parsed_args.email:
271 self.service_account_name = parsed_args.email
xingdai8a00d462018-07-30 14:24:48 -0700272 if parsed_args.service_account_json_private_key_path:
273 self.service_account_json_private_key_path = (
274 parsed_args.service_account_json_private_key_path)
Kevin Chengbced4af2018-06-26 10:35:01 -0700275 if parsed_args.which == "create_gf" and parsed_args.base_image:
276 self.stable_goldfish_host_image_name = parsed_args.base_image
Kevin Chengc9424a82018-10-25 11:34:55 -0700277 if parsed_args.which in [create_args.CMD_CREATE, "create_cf"]:
278 if parsed_args.network:
279 self.network = parsed_args.network
Cody Schuffelen102b3b52019-07-17 10:26:35 -0700280 if parsed_args.multi_stage_launch is not None:
281 self.enable_multi_stage = parsed_args.multi_stage_launch
herbertxueffbcbc22020-07-07 14:35:34 +0800282 if parsed_args.which in [create_args.CMD_CREATE, "create_cf", "create_gf"]:
283 if parsed_args.zone:
284 self.zone = parsed_args.zone
Sam Chiu4f398b72020-03-04 20:43:04 +0800285 if (parsed_args.which == "create_cf" and
286 parsed_args.num_avds_per_instance > 1):
287 scrubbed_args = [arg for arg in self.launch_args.split()
288 if _NUM_INSTANCES_ARG not in arg]
289 scrubbed_args.append("%s=%d" % (_NUM_INSTANCES_ARG,
290 parsed_args.num_avds_per_instance))
291
292 self.launch_args = " ".join(scrubbed_args)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700293
herbertxue908bfa82020-11-24 17:49:26 +0800294 def GetDefaultHwProperty(self, flavor, instance_type=None):
295 """Get default hw configuration values.
herbertxuefd15dfd2018-12-04 11:26:27 +0800296
Sam Chiu1191e2d2020-03-13 13:22:11 +0800297 HwProperty will be overrided according to the change of flavor and
298 instance type. The format of key is flavor or instance_type-flavor.
299 e.g: 'phone' or 'local-phone'.
herbertxue908bfa82020-11-24 17:49:26 +0800300 If the giving key is not found, get hw configuration with a default
Sam Chiu1191e2d2020-03-13 13:22:11 +0800301 phone property.
herbertxuefd15dfd2018-12-04 11:26:27 +0800302
303 Args:
Sam Chiu1191e2d2020-03-13 13:22:11 +0800304 flavor: String of flavor name.
305 instance_type: String of instance type.
herbertxue908bfa82020-11-24 17:49:26 +0800306
307 Returns:
308 String of device hardware property, it would be like
309 "cpu:4,resolution:720x1280,dpi:320,memory:4g".
herbertxuefd15dfd2018-12-04 11:26:27 +0800310 """
Sam Chiu1191e2d2020-03-13 13:22:11 +0800311 hw_key = ("%s-%s" % (instance_type, flavor)
312 if instance_type == constants.INSTANCE_TYPE_LOCAL else flavor)
herbertxue908bfa82020-11-24 17:49:26 +0800313 return self.common_hw_property_map.get(hw_key, _DEFAULT_HW_PROPERTY)
herbertxuefd15dfd2018-12-04 11:26:27 +0800314
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700315 def Verify(self):
316 """Verify configuration fields."""
herbertxuee3c05da2021-04-27 10:37:18 +0800317 missing = self.GetMissingFields(self.REQUIRED_FIELD)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700318 if missing:
319 raise errors.ConfigError(
320 "Missing required configuration fields: %s" % missing)
Sam Chiu46ea3112018-05-18 10:47:52 +0800321 if (self.extra_data_disk_size_gb and self.extra_data_disk_size_gb not in
322 self.precreated_data_image_map):
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700323 raise errors.ConfigError(
324 "Supported extra_data_disk_size_gb options(gb): %s, "
325 "invalid value: %d" % (self.precreated_data_image_map.keys(),
326 self.extra_data_disk_size_gb))
327
herbertxuee3c05da2021-04-27 10:37:18 +0800328 def GetMissingFields(self, fields):
329 """Get missing required fields.
330
331 Args:
332 fields: List of field names.
333
334 Returns:
335 List of missing field names.
336 """
337 return [f for f in fields if not getattr(self, f)]
338
Sam Chiu43d81c52020-02-04 10:49:47 +0800339 def SupportRemoteInstance(self):
340 """Return True if gcp project is provided in config."""
herbertxue908bfa82020-11-24 17:49:26 +0800341 return bool(self.project)
Sam Chiu43d81c52020-02-04 10:49:47 +0800342
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700343
herbertxue36b99f12021-03-25 14:47:28 +0800344class AcloudConfigManager():
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700345 """A class that loads configurations."""
346
347 _DEFAULT_INTERNAL_CONFIG_PATH = os.path.join(_CONFIG_DATA_PATH,
348 "default.config")
349
350 def __init__(self,
351 user_config_path,
352 internal_config_path=_DEFAULT_INTERNAL_CONFIG_PATH):
herbertxue18c9b262018-07-12 19:14:49 +0800353 """Initialize with user specified paths to configs.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700354
355 Args:
356 user_config_path: path to the user config.
357 internal_config_path: path to the internal conifg.
358 """
herbertxue34776bb2018-07-03 21:57:48 +0800359 self.user_config_path = user_config_path
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700360 self._internal_config_path = internal_config_path
361
362 def Load(self):
herbertxue18c9b262018-07-12 19:14:49 +0800363 """Load the configurations.
364
365 Load user config with some special design.
366 1. User specified user config:
367 a.User config exist: Load config.
368 b.User config didn't exist: Raise exception.
369 2. User didn't specify user config, use default config:
370 a.Default config exist: Load config.
371 b.Default config didn't exist: provide empty usr_cfg.
herbertxuec74fbbd2021-08-04 11:34:18 +0800372
373 Raises:
374 errors.ConfigError: If config file doesn't exist.
375
376 Returns:
377 An instance of AcloudConfig.
herbertxue18c9b262018-07-12 19:14:49 +0800378 """
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700379 internal_cfg = None
380 usr_cfg = None
381 try:
382 with open(self._internal_config_path) as config_file:
383 internal_cfg = self.LoadConfigFromProtocolBuffer(
384 config_file, internal_config_pb2.InternalConfig)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700385 except OSError as e:
Sam Chiu46ea3112018-05-18 10:47:52 +0800386 raise errors.ConfigError("Could not load config files: %s" % str(e))
herbertxue18c9b262018-07-12 19:14:49 +0800387 # Load user config file
herbertxuec74fbbd2021-08-04 11:34:18 +0800388 self.user_config_path = GetUserConfigPath(self.user_config_path)
389 if os.path.exists(self.user_config_path):
390 with open(self.user_config_path, "r") as config_file:
391 usr_cfg = self.LoadConfigFromProtocolBuffer(
392 config_file, user_config_pb2.UserConfig)
herbertxue18c9b262018-07-12 19:14:49 +0800393 else:
herbertxuec74fbbd2021-08-04 11:34:18 +0800394 if self.user_config_path != GetDefaultConfigFile():
395 raise errors.ConfigError(
396 "The config file doesn't exist: %s. For reset config "
397 "information: go/acloud-googler-setup#reset-configuration" %
398 (self.user_config_path))
399 usr_cfg = user_config_pb2.UserConfig()
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700400 return AcloudConfig(usr_cfg, internal_cfg)
401
402 @staticmethod
403 def LoadConfigFromProtocolBuffer(config_file, message_type):
404 """Load config from a text-based protocol buffer file.
405
406 Args:
407 config_file: A python File object.
408 message_type: A proto message class.
409
410 Returns:
411 An instance of type "message_type" populated with data
412 from the file.
413 """
414 try:
415 config = message_type()
416 text_format.Merge(config_file.read(), config)
417 return config
418 except text_format.ParseError as e:
419 raise errors.ConfigError("Could not parse config: %s" % str(e))