1#!/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 37TODO: 38 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 importlib.resources 48import os 49 50from google.protobuf import text_format 51 52# pylint: disable=no-name-in-module,import-error 53from acloud import errors 54from acloud.internal import constants 55from acloud.internal.proto import internal_config_pb2 56from acloud.internal.proto import user_config_pb2 57from acloud.create import create_args 58 59 60logger = logging.getLogger(__name__) 61 62_DEFAULT_CONFIG_FILE = "acloud.config" 63_DEFAULT_HW_PROPERTY = "cpu:4,resolution:720x1280,dpi:320,memory:4g" 64 65# Resources 66_INTERNAL_CONFIG_FILE = "default.config" 67_VERSION_FILE = "VERSION" 68_UNKNOWN = "UNKNOWN" 69_NUM_INSTANCES_ARG = "-num_instances" 70 71 72def _OpenTextResource(resource): 73 # Acloud binary does not have the embedded launcher, and it should be 74 # compatible with python3.7 installed on the test servers. 75 return importlib.resources.open_text("acloud.public.data", resource) 76 77 78def GetVersion(): 79 """Print the version of acloud. 80 81 The VERSION file is built into the acloud binary. The version file path is 82 under "public/data". 83 84 Returns: 85 String of the acloud version. 86 """ 87 try: 88 with _OpenTextResource(_VERSION_FILE) as version_file: 89 return version_file.read() 90 except FileNotFoundError: 91 return _UNKNOWN 92 93 94def GetDefaultConfigFile(): 95 """Return path to default config file.""" 96 config_path = os.path.join(os.path.expanduser("~"), ".config", "acloud") 97 # Create the default config dir if it doesn't exist. 98 if not os.path.exists(config_path): 99 os.makedirs(config_path) 100 return os.path.join(config_path, _DEFAULT_CONFIG_FILE) 101 102 103def GetUserConfigPath(config_path): 104 """Get Acloud user config file path. 105 106 If there is no config provided, Acloud would use default config path. 107 108 Args: 109 config_path: String, path of Acloud config file. 110 111 Returns: 112 Path (string) of the Acloud config. 113 """ 114 if config_path: 115 return config_path 116 return GetDefaultConfigFile() 117 118 119def GetAcloudConfig(args): 120 """Helper function to initialize Config object. 121 122 Args: 123 args: Namespace object from argparse.parse_args. 124 125 Return: 126 An instance of AcloudConfig. 127 """ 128 config_mgr = AcloudConfigManager(args.config_file) 129 cfg = config_mgr.Load() 130 cfg.OverrideWithArgs(args) 131 return cfg 132 133 134class AcloudConfig(): 135 """A class that holds all configurations for acloud.""" 136 137 REQUIRED_FIELD = [ 138 "machine_type", "network", "min_machine_size", 139 "disk_image_name", "disk_image_mime_type" 140 ] 141 142 # pylint: disable=too-many-statements 143 def __init__(self, usr_cfg, internal_cfg): 144 """Initialize. 145 146 Args: 147 usr_cfg: A protobuf object that holds the user configurations. 148 internal_cfg: A protobuf object that holds internal configurations. 149 """ 150 self.service_account_name = usr_cfg.service_account_name 151 # pylint: disable=invalid-name 152 self.service_account_private_key_path = ( 153 usr_cfg.service_account_private_key_path) 154 self.service_account_json_private_key_path = ( 155 usr_cfg.service_account_json_private_key_path) 156 self.creds_cache_file = internal_cfg.creds_cache_file 157 self.user_agent = internal_cfg.user_agent 158 self.client_id = usr_cfg.client_id 159 self.client_secret = usr_cfg.client_secret 160 161 self.project = usr_cfg.project 162 self.zone = usr_cfg.zone 163 self.machine_type = (usr_cfg.machine_type or 164 internal_cfg.default_usr_cfg.machine_type) 165 self.network = usr_cfg.network or internal_cfg.default_usr_cfg.network 166 self.ssh_private_key_path = usr_cfg.ssh_private_key_path 167 self.ssh_public_key_path = usr_cfg.ssh_public_key_path 168 self.storage_bucket_name = usr_cfg.storage_bucket_name 169 self.metadata_variable = dict( 170 internal_cfg.default_usr_cfg.metadata_variable.items()) 171 self.metadata_variable.update(usr_cfg.metadata_variable) 172 173 self.device_resolution_map = dict( 174 internal_cfg.device_resolution_map.items()) 175 self.device_default_orientation_map = dict( 176 internal_cfg.device_default_orientation_map.items()) 177 self.no_project_access_msg_map = dict( 178 internal_cfg.no_project_access_msg_map.items()) 179 self.min_machine_size = internal_cfg.min_machine_size 180 self.disk_image_name = internal_cfg.disk_image_name 181 self.disk_image_mime_type = internal_cfg.disk_image_mime_type 182 self.disk_image_extension = internal_cfg.disk_image_extension 183 self.disk_raw_image_name = internal_cfg.disk_raw_image_name 184 self.disk_raw_image_extension = internal_cfg.disk_raw_image_extension 185 self.valid_branch_and_min_build_id = dict( 186 internal_cfg.valid_branch_and_min_build_id.items()) 187 self.precreated_data_image_map = dict( 188 internal_cfg.precreated_data_image.items()) 189 self.extra_data_disk_size_gb = ( 190 usr_cfg.extra_data_disk_size_gb or 191 internal_cfg.default_usr_cfg.extra_data_disk_size_gb) 192 if self.extra_data_disk_size_gb > 0: 193 if "cfg_sta_persistent_data_device" not in usr_cfg.metadata_variable: 194 # If user did not set it explicity, use default. 195 self.metadata_variable["cfg_sta_persistent_data_device"] = ( 196 internal_cfg.default_extra_data_disk_device) 197 if "cfg_sta_ephemeral_data_size_mb" in usr_cfg.metadata_variable: 198 raise errors.ConfigError( 199 "The following settings can't be set at the same time: " 200 "extra_data_disk_size_gb and" 201 "metadata variable cfg_sta_ephemeral_data_size_mb.") 202 if "cfg_sta_ephemeral_data_size_mb" in self.metadata_variable: 203 del self.metadata_variable["cfg_sta_ephemeral_data_size_mb"] 204 205 # Additional scopes to be passed to the created instance 206 self.extra_scopes = usr_cfg.extra_scopes 207 208 # Fields that can be overriden by args 209 self.orientation = usr_cfg.orientation 210 self.resolution = usr_cfg.resolution 211 212 self.stable_host_image_family = usr_cfg.stable_host_image_family 213 self.stable_host_image_name = ( 214 usr_cfg.stable_host_image_name or 215 internal_cfg.default_usr_cfg.stable_host_image_name) 216 self.stable_host_image_project = ( 217 usr_cfg.stable_host_image_project or 218 internal_cfg.default_usr_cfg.stable_host_image_project) 219 self.kernel_build_target = internal_cfg.kernel_build_target 220 221 self.emulator_build_target = internal_cfg.emulator_build_target 222 self.stable_goldfish_host_image_name = ( 223 usr_cfg.stable_goldfish_host_image_name or 224 internal_cfg.default_usr_cfg.stable_goldfish_host_image_name) 225 self.stable_goldfish_host_image_project = ( 226 usr_cfg.stable_goldfish_host_image_project or 227 internal_cfg.default_usr_cfg.stable_goldfish_host_image_project) 228 229 self.stable_cheeps_host_image_name = ( 230 usr_cfg.stable_cheeps_host_image_name or 231 internal_cfg.default_usr_cfg.stable_cheeps_host_image_name) 232 self.stable_cheeps_host_image_project = ( 233 usr_cfg.stable_cheeps_host_image_project or 234 internal_cfg.default_usr_cfg.stable_cheeps_host_image_project) 235 self.betty_image = usr_cfg.betty_image 236 237 self.extra_args_ssh_tunnel = usr_cfg.extra_args_ssh_tunnel 238 239 self.common_hw_property_map = internal_cfg.common_hw_property_map 240 self.hw_property = usr_cfg.hw_property 241 242 self.launch_args = usr_cfg.launch_args 243 self.oxygen_client = usr_cfg.oxygen_client 244 self.oxygen_lease_args = usr_cfg.oxygen_lease_args 245 self.connect_hostname = usr_cfg.connect_hostname 246 self.instance_name_pattern = ( 247 usr_cfg.instance_name_pattern or 248 internal_cfg.default_usr_cfg.instance_name_pattern) 249 self.fetch_cvd_version = ( 250 usr_cfg.fetch_cvd_version or 251 internal_cfg.default_usr_cfg.fetch_cvd_version) 252 if usr_cfg.HasField("enable_multi_stage") is not None: 253 self.enable_multi_stage = usr_cfg.enable_multi_stage 254 elif internal_cfg.default_usr_cfg.HasField("enable_multi_stage"): 255 self.enable_multi_stage = internal_cfg.default_usr_cfg.enable_multi_stage 256 else: 257 self.enable_multi_stage = False 258 self.disk_type = usr_cfg.disk_type 259 self.use_cvdr = usr_cfg.use_cvdr 260 self.use_legacy_acloud = usr_cfg.use_legacy_acloud 261 262 # Verify validity of configurations. 263 self.Verify() 264 265 # pylint: disable=too-many-branches 266 def OverrideWithArgs(self, parsed_args): 267 """Override configuration values with args passed in from cmd line. 268 269 Args: 270 parsed_args: Args parsed from command line. 271 """ 272 if parsed_args.which == create_args.CMD_CREATE and parsed_args.spec: 273 if not self.resolution: 274 self.resolution = self.device_resolution_map.get( 275 parsed_args.spec, "") 276 if not self.orientation: 277 self.orientation = self.device_default_orientation_map.get( 278 parsed_args.spec, "") 279 if parsed_args.email: 280 self.service_account_name = parsed_args.email 281 if parsed_args.service_account_json_private_key_path: 282 self.service_account_json_private_key_path = ( 283 parsed_args.service_account_json_private_key_path) 284 if parsed_args.which == "create_gf" and parsed_args.base_image: 285 self.stable_goldfish_host_image_name = parsed_args.base_image 286 if parsed_args.which in [create_args.CMD_CREATE, "create_cf"]: 287 if parsed_args.network: 288 self.network = parsed_args.network 289 if parsed_args.multi_stage_launch is not None: 290 self.enable_multi_stage = parsed_args.multi_stage_launch 291 if parsed_args.which in [create_args.CMD_CREATE, "create_cf", "create_gf"]: 292 if parsed_args.zone: 293 self.zone = parsed_args.zone 294 if (parsed_args.which == "create_cf" and 295 parsed_args.num_avds_per_instance > 1): 296 scrubbed_args = [arg for arg in self.launch_args.split() 297 if _NUM_INSTANCES_ARG not in arg] 298 scrubbed_args.append("%s=%d" % (_NUM_INSTANCES_ARG, 299 parsed_args.num_avds_per_instance)) 300 301 self.launch_args = " ".join(scrubbed_args) 302 303 def GetDefaultHwProperty(self, flavor, instance_type=None): 304 """Get default hw configuration values. 305 306 HwProperty will be overrided according to the change of flavor and 307 instance type. The format of key is flavor or instance_type-flavor. 308 e.g: 'phone' or 'local-phone'. 309 If the giving key is not found, get hw configuration with a default 310 phone property. 311 312 Args: 313 flavor: String of flavor name. 314 instance_type: String of instance type. 315 316 Returns: 317 String of device hardware property, it would be like 318 "cpu:4,resolution:720x1280,dpi:320,memory:4g". 319 """ 320 hw_key = ("%s-%s" % (instance_type, flavor) 321 if instance_type == constants.INSTANCE_TYPE_LOCAL else flavor) 322 return self.common_hw_property_map.get(hw_key, _DEFAULT_HW_PROPERTY) 323 324 def Verify(self): 325 """Verify configuration fields.""" 326 missing = self.GetMissingFields(self.REQUIRED_FIELD) 327 if missing: 328 raise errors.ConfigError( 329 "Missing required configuration fields: %s" % missing) 330 if (self.extra_data_disk_size_gb and self.extra_data_disk_size_gb not in 331 self.precreated_data_image_map): 332 raise errors.ConfigError( 333 "Supported extra_data_disk_size_gb options(gb): %s, " 334 "invalid value: %d" % (self.precreated_data_image_map.keys(), 335 self.extra_data_disk_size_gb)) 336 337 def GetMissingFields(self, fields): 338 """Get missing required fields. 339 340 Args: 341 fields: List of field names. 342 343 Returns: 344 List of missing field names. 345 """ 346 return [f for f in fields if not getattr(self, f)] 347 348 def SupportRemoteInstance(self): 349 """Return True if gcp project is provided in config.""" 350 return bool(self.project) 351 352 353class AcloudConfigManager(): 354 """A class that loads configurations.""" 355 356 def __init__(self, user_config_path): 357 """Initialize with user specified paths to configs. 358 359 Args: 360 user_config_path: path to the user config. 361 """ 362 self.user_config_path = user_config_path 363 364 def Load(self): 365 """Load the configurations. 366 367 Load user config with some special design. 368 1. User specified user config: 369 a.User config exist: Load config. 370 b.User config didn't exist: Raise exception. 371 2. User didn't specify user config, use default config: 372 a.Default config exist: Load config. 373 b.Default config didn't exist: provide empty usr_cfg. 374 375 Raises: 376 errors.ConfigError: If config file doesn't exist. 377 378 Returns: 379 An instance of AcloudConfig. 380 """ 381 internal_cfg = None 382 usr_cfg = None 383 try: 384 with _OpenTextResource(_INTERNAL_CONFIG_FILE) as cfg_file: 385 internal_cfg = self.LoadConfigFromProtocolBuffer( 386 cfg_file, internal_config_pb2.InternalConfig) 387 except OSError as e: 388 raise errors.ConfigError("Could not load config files: %s" % str(e)) 389 # Load user config file 390 self.user_config_path = GetUserConfigPath(self.user_config_path) 391 if os.path.exists(self.user_config_path): 392 with open(self.user_config_path, "r") as config_file: 393 usr_cfg = self.LoadConfigFromProtocolBuffer( 394 config_file, user_config_pb2.UserConfig) 395 else: 396 if self.user_config_path != GetDefaultConfigFile(): 397 raise errors.ConfigError( 398 "The config file doesn't exist: %s. For reset config " 399 "information: go/acloud-googler-setup#reset-configuration" % 400 (self.user_config_path)) 401 usr_cfg = user_config_pb2.UserConfig() 402 return AcloudConfig(usr_cfg, internal_cfg) 403 404 @staticmethod 405 def LoadConfigFromProtocolBuffer(config_file, message_type): 406 """Load config from a text-based protocol buffer file. 407 408 Args: 409 config_file: A python File object. 410 message_type: A proto message class. 411 412 Returns: 413 An instance of type "message_type" populated with data 414 from the file. 415 """ 416 try: 417 config = message_type() 418 text_format.Merge(config_file.read(), config) 419 return config 420 except text_format.ParseError as e: 421 raise errors.ConfigError("Could not parse config: %s" % str(e)) 422