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:2,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(object): 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 228 self.extra_args_ssh_tunnel = usr_cfg.extra_args_ssh_tunnel 229 230 self.common_hw_property_map = internal_cfg.common_hw_property_map 231 self.hw_property = usr_cfg.hw_property 232 233 self.launch_args = usr_cfg.launch_args 234 self.instance_name_pattern = ( 235 usr_cfg.instance_name_pattern or 236 internal_cfg.default_usr_cfg.instance_name_pattern) 237 self.fetch_cvd_version = ( 238 usr_cfg.fetch_cvd_version or 239 internal_cfg.default_usr_cfg.fetch_cvd_version) 240 if usr_cfg.HasField("enable_multi_stage") is not None: 241 self.enable_multi_stage = usr_cfg.enable_multi_stage 242 elif internal_cfg.default_usr_cfg.HasField("enable_multi_stage"): 243 self.enable_multi_stage = internal_cfg.default_usr_cfg.enable_multi_stage 244 else: 245 self.enable_multi_stage = False 246 247 # Verify validity of configurations. 248 self.Verify() 249 250 # pylint: disable=too-many-branches 251 def OverrideWithArgs(self, parsed_args): 252 """Override configuration values with args passed in from cmd line. 253 254 Args: 255 parsed_args: Args parsed from command line. 256 """ 257 if parsed_args.which == create_args.CMD_CREATE and parsed_args.spec: 258 if not self.resolution: 259 self.resolution = self.device_resolution_map.get( 260 parsed_args.spec, "") 261 if not self.orientation: 262 self.orientation = self.device_default_orientation_map.get( 263 parsed_args.spec, "") 264 if parsed_args.email: 265 self.service_account_name = parsed_args.email 266 if parsed_args.service_account_json_private_key_path: 267 self.service_account_json_private_key_path = ( 268 parsed_args.service_account_json_private_key_path) 269 if parsed_args.which == "create_gf" and parsed_args.base_image: 270 self.stable_goldfish_host_image_name = parsed_args.base_image 271 if parsed_args.which == create_args.CMD_CREATE and not self.hw_property: 272 flavor = parsed_args.flavor or constants.FLAVOR_PHONE 273 self.hw_property = self.common_hw_property_map.get(flavor, "") 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 OverrideHwProperty(self, flavor, instance_type=None): 292 """Override 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, set 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 hw_key = ("%s-%s" % (instance_type, flavor) 305 if instance_type == constants.INSTANCE_TYPE_LOCAL else flavor) 306 self.hw_property = self.common_hw_property_map.get( 307 hw_key, _DEFAULT_HW_PROPERTY) 308 309 def Verify(self): 310 """Verify configuration fields.""" 311 missing = [f for f in self.REQUIRED_FIELD if not getattr(self, f)] 312 if missing: 313 raise errors.ConfigError( 314 "Missing required configuration fields: %s" % missing) 315 if (self.extra_data_disk_size_gb and self.extra_data_disk_size_gb not in 316 self.precreated_data_image_map): 317 raise errors.ConfigError( 318 "Supported extra_data_disk_size_gb options(gb): %s, " 319 "invalid value: %d" % (self.precreated_data_image_map.keys(), 320 self.extra_data_disk_size_gb)) 321 322 def SupportRemoteInstance(self): 323 """Return True if gcp project is provided in config.""" 324 return True if self.project else False 325 326 327class AcloudConfigManager(object): 328 """A class that loads configurations.""" 329 330 _DEFAULT_INTERNAL_CONFIG_PATH = os.path.join(_CONFIG_DATA_PATH, 331 "default.config") 332 333 def __init__(self, 334 user_config_path, 335 internal_config_path=_DEFAULT_INTERNAL_CONFIG_PATH): 336 """Initialize with user specified paths to configs. 337 338 Args: 339 user_config_path: path to the user config. 340 internal_config_path: path to the internal conifg. 341 """ 342 self.user_config_path = user_config_path 343 self._internal_config_path = internal_config_path 344 345 def Load(self): 346 """Load the configurations. 347 348 Load user config with some special design. 349 1. User specified user config: 350 a.User config exist: Load config. 351 b.User config didn't exist: Raise exception. 352 2. User didn't specify user config, use default config: 353 a.Default config exist: Load config. 354 b.Default config didn't exist: provide empty usr_cfg. 355 """ 356 internal_cfg = None 357 usr_cfg = None 358 try: 359 with open(self._internal_config_path) as config_file: 360 internal_cfg = self.LoadConfigFromProtocolBuffer( 361 config_file, internal_config_pb2.InternalConfig) 362 except OSError as e: 363 raise errors.ConfigError("Could not load config files: %s" % str(e)) 364 # Load user config file 365 if self.user_config_path: 366 if os.path.exists(self.user_config_path): 367 with open(self.user_config_path, "r") as config_file: 368 usr_cfg = self.LoadConfigFromProtocolBuffer( 369 config_file, user_config_pb2.UserConfig) 370 else: 371 raise errors.ConfigError("The file doesn't exist: %s" % 372 (self.user_config_path)) 373 else: 374 self.user_config_path = GetDefaultConfigFile() 375 if os.path.exists(self.user_config_path): 376 with open(self.user_config_path, "r") as config_file: 377 usr_cfg = self.LoadConfigFromProtocolBuffer( 378 config_file, user_config_pb2.UserConfig) 379 else: 380 usr_cfg = user_config_pb2.UserConfig() 381 return AcloudConfig(usr_cfg, internal_cfg) 382 383 @staticmethod 384 def LoadConfigFromProtocolBuffer(config_file, message_type): 385 """Load config from a text-based protocol buffer file. 386 387 Args: 388 config_file: A python File object. 389 message_type: A proto message class. 390 391 Returns: 392 An instance of type "message_type" populated with data 393 from the file. 394 """ 395 try: 396 config = message_type() 397 text_format.Merge(config_file.read(), config) 398 return config 399 except text_format.ParseError as e: 400 raise errors.ConfigError("Could not parse config: %s" % str(e)) 401