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 if usr_cfg.HasField("enable_multi_stage") is not None: 250 self.enable_multi_stage = usr_cfg.enable_multi_stage 251 elif internal_cfg.default_usr_cfg.HasField("enable_multi_stage"): 252 self.enable_multi_stage = internal_cfg.default_usr_cfg.enable_multi_stage 253 else: 254 self.enable_multi_stage = False 255 self.disk_type = usr_cfg.disk_type 256 self.use_cvdr = usr_cfg.use_cvdr 257 self.use_legacy_acloud = usr_cfg.use_legacy_acloud 258 259 # Verify validity of configurations. 260 self.Verify() 261 262 # pylint: disable=too-many-branches 263 def OverrideWithArgs(self, parsed_args): 264 """Override configuration values with args passed in from cmd line. 265 266 Args: 267 parsed_args: Args parsed from command line. 268 """ 269 if parsed_args.which == create_args.CMD_CREATE and parsed_args.spec: 270 if not self.resolution: 271 self.resolution = self.device_resolution_map.get( 272 parsed_args.spec, "") 273 if not self.orientation: 274 self.orientation = self.device_default_orientation_map.get( 275 parsed_args.spec, "") 276 if parsed_args.email: 277 self.service_account_name = parsed_args.email 278 if parsed_args.service_account_json_private_key_path: 279 self.service_account_json_private_key_path = ( 280 parsed_args.service_account_json_private_key_path) 281 if parsed_args.which == "create_gf" and parsed_args.base_image: 282 self.stable_goldfish_host_image_name = parsed_args.base_image 283 if parsed_args.which in [create_args.CMD_CREATE, "create_cf"]: 284 if parsed_args.network: 285 self.network = parsed_args.network 286 if parsed_args.multi_stage_launch is not None: 287 self.enable_multi_stage = parsed_args.multi_stage_launch 288 if parsed_args.which in [create_args.CMD_CREATE, "create_cf", "create_gf"]: 289 if parsed_args.zone: 290 self.zone = parsed_args.zone 291 if (parsed_args.which == "create_cf" and 292 parsed_args.num_avds_per_instance > 1): 293 scrubbed_args = [arg for arg in self.launch_args.split() 294 if _NUM_INSTANCES_ARG not in arg] 295 scrubbed_args.append("%s=%d" % (_NUM_INSTANCES_ARG, 296 parsed_args.num_avds_per_instance)) 297 298 self.launch_args = " ".join(scrubbed_args) 299 300 def GetDefaultHwProperty(self, flavor, instance_type=None): 301 """Get default hw configuration values. 302 303 HwProperty will be overrided according to the change of flavor and 304 instance type. The format of key is flavor or instance_type-flavor. 305 e.g: 'phone' or 'local-phone'. 306 If the giving key is not found, get hw configuration with a default 307 phone property. 308 309 Args: 310 flavor: String of flavor name. 311 instance_type: String of instance type. 312 313 Returns: 314 String of device hardware property, it would be like 315 "cpu:4,resolution:720x1280,dpi:320,memory:4g". 316 """ 317 hw_key = ("%s-%s" % (instance_type, flavor) 318 if instance_type == constants.INSTANCE_TYPE_LOCAL else flavor) 319 return self.common_hw_property_map.get(hw_key, _DEFAULT_HW_PROPERTY) 320 321 def Verify(self): 322 """Verify configuration fields.""" 323 missing = self.GetMissingFields(self.REQUIRED_FIELD) 324 if missing: 325 raise errors.ConfigError( 326 "Missing required configuration fields: %s" % missing) 327 if (self.extra_data_disk_size_gb and self.extra_data_disk_size_gb not in 328 self.precreated_data_image_map): 329 raise errors.ConfigError( 330 "Supported extra_data_disk_size_gb options(gb): %s, " 331 "invalid value: %d" % (self.precreated_data_image_map.keys(), 332 self.extra_data_disk_size_gb)) 333 334 def GetMissingFields(self, fields): 335 """Get missing required fields. 336 337 Args: 338 fields: List of field names. 339 340 Returns: 341 List of missing field names. 342 """ 343 return [f for f in fields if not getattr(self, f)] 344 345 def SupportRemoteInstance(self): 346 """Return True if gcp project is provided in config.""" 347 return bool(self.project) 348 349 350class AcloudConfigManager(): 351 """A class that loads configurations.""" 352 353 def __init__(self, user_config_path): 354 """Initialize with user specified paths to configs. 355 356 Args: 357 user_config_path: path to the user config. 358 """ 359 self.user_config_path = user_config_path 360 361 def Load(self): 362 """Load the configurations. 363 364 Load user config with some special design. 365 1. User specified user config: 366 a.User config exist: Load config. 367 b.User config didn't exist: Raise exception. 368 2. User didn't specify user config, use default config: 369 a.Default config exist: Load config. 370 b.Default config didn't exist: provide empty usr_cfg. 371 372 Raises: 373 errors.ConfigError: If config file doesn't exist. 374 375 Returns: 376 An instance of AcloudConfig. 377 """ 378 internal_cfg = None 379 usr_cfg = None 380 try: 381 with _OpenTextResource(_INTERNAL_CONFIG_FILE) as cfg_file: 382 internal_cfg = self.LoadConfigFromProtocolBuffer( 383 cfg_file, internal_config_pb2.InternalConfig) 384 except OSError as e: 385 raise errors.ConfigError("Could not load config files: %s" % str(e)) 386 # Load user config file 387 self.user_config_path = GetUserConfigPath(self.user_config_path) 388 if os.path.exists(self.user_config_path): 389 with open(self.user_config_path, "r") as config_file: 390 usr_cfg = self.LoadConfigFromProtocolBuffer( 391 config_file, user_config_pb2.UserConfig) 392 else: 393 if self.user_config_path != GetDefaultConfigFile(): 394 raise errors.ConfigError( 395 "The config file doesn't exist: %s. For reset config " 396 "information: go/acloud-googler-setup#reset-configuration" % 397 (self.user_config_path)) 398 usr_cfg = user_config_pb2.UserConfig() 399 return AcloudConfig(usr_cfg, internal_cfg) 400 401 @staticmethod 402 def LoadConfigFromProtocolBuffer(config_file, message_type): 403 """Load config from a text-based protocol buffer file. 404 405 Args: 406 config_file: A python File object. 407 message_type: A proto message class. 408 409 Returns: 410 An instance of type "message_type" populated with data 411 from the file. 412 """ 413 try: 414 config = message_type() 415 text_format.Merge(config_file.read(), config) 416 return config 417 except text_format.ParseError as e: 418 raise errors.ConfigError("Could not parse config: %s" % str(e)) 419