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 os 48 49import six 50 51from google.protobuf import text_format 52 53# pylint: disable=no-name-in-module,import-error 54from acloud import errors 55from acloud.internal import constants 56from acloud.internal.proto import internal_config_pb2 57from acloud.internal.proto import user_config_pb2 58from acloud.create import create_args 59 60 61logger = logging.getLogger(__name__) 62 63_CONFIG_DATA_PATH = os.path.join( 64 os.path.dirname(os.path.abspath(__file__)), "data") 65_DEFAULT_CONFIG_FILE = "acloud.config" 66_DEFAULT_HW_PROPERTY = "cpu:4,resolution:720x1280,dpi:320,memory:4g" 67 68# VERSION 69_VERSION_FILE = "VERSION" 70_UNKNOWN = "UNKNOWN" 71_NUM_INSTANCES_ARG = "-num_instances" 72 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 89 90def GetDefaultConfigFile(): 91 """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) 96 return os.path.join(config_path, _DEFAULT_CONFIG_FILE) 97 98 99def 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 114class AcloudConfig(): 115 """A class that holds all configurations for acloud.""" 116 117 REQUIRED_FIELD = [ 118 "machine_type", "network", "min_machine_size", 119 "disk_image_name", "disk_image_mime_type" 120 ] 121 122 # pylint: disable=too-many-statements 123 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 131 # pylint: disable=invalid-name 132 self.service_account_private_key_path = ( 133 usr_cfg.service_account_private_key_path) 134 self.service_account_json_private_key_path = ( 135 usr_cfg.service_account_json_private_key_path) 136 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) 145 self.network = usr_cfg.network or internal_cfg.default_usr_cfg.network 146 self.ssh_private_key_path = usr_cfg.ssh_private_key_path 147 self.ssh_public_key_path = usr_cfg.ssh_public_key_path 148 self.storage_bucket_name = usr_cfg.storage_bucket_name 149 self.metadata_variable = { 150 key: val for key, val in 151 six.iteritems(internal_cfg.default_usr_cfg.metadata_variable) 152 } 153 self.metadata_variable.update(usr_cfg.metadata_variable) 154 155 self.device_resolution_map = { 156 device: resolution for device, resolution in 157 six.iteritems(internal_cfg.device_resolution_map) 158 } 159 self.device_default_orientation_map = { 160 device: orientation for device, orientation in 161 six.iteritems(internal_cfg.device_default_orientation_map) 162 } 163 self.no_project_access_msg_map = { 164 project: msg for project, msg in 165 six.iteritems(internal_cfg.no_project_access_msg_map) 166 } 167 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 = { 174 branch: min_build_id for branch, min_build_id in 175 six.iteritems(internal_cfg.valid_branch_and_min_build_id) 176 } 177 self.precreated_data_image_map = { 178 size_gb: image_name for size_gb, image_name in 179 six.iteritems(internal_cfg.precreated_data_image) 180 } 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 197 # Additional scopes to be passed to the created instance 198 self.extra_scopes = usr_cfg.extra_scopes 199 200 # Fields that can be overriden by args 201 self.orientation = usr_cfg.orientation 202 self.resolution = usr_cfg.resolution 203 204 self.stable_host_image_family = usr_cfg.stable_host_image_family 205 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 221 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) 227 self.betty_image = usr_cfg.betty_image 228 229 self.extra_args_ssh_tunnel = usr_cfg.extra_args_ssh_tunnel 230 231 self.common_hw_property_map = internal_cfg.common_hw_property_map 232 self.hw_property = usr_cfg.hw_property 233 234 self.launch_args = usr_cfg.launch_args 235 self.api_key = usr_cfg.api_key 236 self.api_url = usr_cfg.api_url 237 self.instance_name_pattern = ( 238 usr_cfg.instance_name_pattern or 239 internal_cfg.default_usr_cfg.instance_name_pattern) 240 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 249 250 # Verify validity of configurations. 251 self.Verify() 252 253 # pylint: disable=too-many-branches 254 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 """ 260 if parsed_args.which == create_args.CMD_CREATE and parsed_args.spec: 261 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 269 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) 272 if parsed_args.which == "create_gf" and parsed_args.base_image: 273 self.stable_goldfish_host_image_name = parsed_args.base_image 274 if parsed_args.which in [create_args.CMD_CREATE, "create_cf"]: 275 if parsed_args.network: 276 self.network = parsed_args.network 277 if parsed_args.multi_stage_launch is not None: 278 self.enable_multi_stage = parsed_args.multi_stage_launch 279 if parsed_args.which in [create_args.CMD_CREATE, "create_cf", "create_gf"]: 280 if parsed_args.zone: 281 self.zone = parsed_args.zone 282 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) 290 291 def GetDefaultHwProperty(self, flavor, instance_type=None): 292 """Get default hw configuration values. 293 294 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'. 297 If the giving key is not found, get hw configuration with a default 298 phone property. 299 300 Args: 301 flavor: String of flavor name. 302 instance_type: String of instance type. 303 304 Returns: 305 String of device hardware property, it would be like 306 "cpu:4,resolution:720x1280,dpi:320,memory:4g". 307 """ 308 hw_key = ("%s-%s" % (instance_type, flavor) 309 if instance_type == constants.INSTANCE_TYPE_LOCAL else flavor) 310 return self.common_hw_property_map.get(hw_key, _DEFAULT_HW_PROPERTY) 311 312 def Verify(self): 313 """Verify configuration fields.""" 314 missing = self.GetMissingFields(self.REQUIRED_FIELD) 315 if missing: 316 raise errors.ConfigError( 317 "Missing required configuration fields: %s" % missing) 318 if (self.extra_data_disk_size_gb and self.extra_data_disk_size_gb not in 319 self.precreated_data_image_map): 320 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 325 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 336 def SupportRemoteInstance(self): 337 """Return True if gcp project is provided in config.""" 338 return bool(self.project) 339 340 341class AcloudConfigManager(): 342 """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): 350 """Initialize with user specified paths to configs. 351 352 Args: 353 user_config_path: path to the user config. 354 internal_config_path: path to the internal conifg. 355 """ 356 self.user_config_path = user_config_path 357 self._internal_config_path = internal_config_path 358 359 def Load(self): 360 """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 """ 370 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) 376 except OSError as e: 377 raise errors.ConfigError("Could not load config files: %s" % str(e)) 378 # Load user config file 379 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: 382 usr_cfg = self.LoadConfigFromProtocolBuffer( 383 config_file, user_config_pb2.UserConfig) 384 else: 385 raise errors.ConfigError("The file doesn't exist: %s" % 386 (self.user_config_path)) 387 else: 388 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: 391 usr_cfg = self.LoadConfigFromProtocolBuffer( 392 config_file, user_config_pb2.UserConfig) 393 else: 394 usr_cfg = user_config_pb2.UserConfig() 395 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)) 415