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 49from google.protobuf import text_format 50 51# pylint: disable=no-name-in-module,import-error 52from acloud import errors 53from acloud.internal import constants 54from acloud.internal.proto import internal_config_pb2 55from acloud.internal.proto import user_config_pb2 56from acloud.create import create_args 57 58 59logger = logging.getLogger(__name__) 60 61_CONFIG_DATA_PATH = os.path.join( 62 os.path.dirname(os.path.abspath(__file__)), "data") 63_DEFAULT_CONFIG_FILE = "acloud.config" 64_DEFAULT_HW_PROPERTY = "cpu:4,resolution:720x1280,dpi:320,memory:4g" 65 66# VERSION 67_VERSION_FILE = "VERSION" 68_UNKNOWN = "UNKNOWN" 69_NUM_INSTANCES_ARG = "-num_instances" 70 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 87 88def GetDefaultConfigFile(): 89 """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) 94 return os.path.join(config_path, _DEFAULT_CONFIG_FILE) 95 96 97def 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 113def 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 128class AcloudConfig(): 129 """A class that holds all configurations for acloud.""" 130 131 REQUIRED_FIELD = [ 132 "machine_type", "network", "min_machine_size", 133 "disk_image_name", "disk_image_mime_type" 134 ] 135 136 # pylint: disable=too-many-statements 137 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 145 # pylint: disable=invalid-name 146 self.service_account_private_key_path = ( 147 usr_cfg.service_account_private_key_path) 148 self.service_account_json_private_key_path = ( 149 usr_cfg.service_account_json_private_key_path) 150 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) 159 self.network = usr_cfg.network or internal_cfg.default_usr_cfg.network 160 self.ssh_private_key_path = usr_cfg.ssh_private_key_path 161 self.ssh_public_key_path = usr_cfg.ssh_public_key_path 162 self.storage_bucket_name = usr_cfg.storage_bucket_name 163 self.metadata_variable = dict( 164 internal_cfg.default_usr_cfg.metadata_variable.items()) 165 self.metadata_variable.update(usr_cfg.metadata_variable) 166 167 self.device_resolution_map = dict( 168 internal_cfg.device_resolution_map.items()) 169 self.device_default_orientation_map = dict( 170 internal_cfg.device_default_orientation_map.items()) 171 self.no_project_access_msg_map = dict( 172 internal_cfg.no_project_access_msg_map.items()) 173 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 179 self.valid_branch_and_min_build_id = dict( 180 internal_cfg.valid_branch_and_min_build_id.items()) 181 self.precreated_data_image_map = dict( 182 internal_cfg.precreated_data_image.items()) 183 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 199 # Additional scopes to be passed to the created instance 200 self.extra_scopes = usr_cfg.extra_scopes 201 202 # Fields that can be overriden by args 203 self.orientation = usr_cfg.orientation 204 self.resolution = usr_cfg.resolution 205 206 self.stable_host_image_family = usr_cfg.stable_host_image_family 207 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 223 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) 229 self.betty_image = usr_cfg.betty_image 230 231 self.extra_args_ssh_tunnel = usr_cfg.extra_args_ssh_tunnel 232 233 self.common_hw_property_map = internal_cfg.common_hw_property_map 234 self.hw_property = usr_cfg.hw_property 235 236 self.launch_args = usr_cfg.launch_args 237 self.oxygen_client = usr_cfg.oxygen_client 238 self.oxygen_lease_args = usr_cfg.oxygen_lease_args 239 self.connect_hostname = usr_cfg.connect_hostname 240 self.instance_name_pattern = ( 241 usr_cfg.instance_name_pattern or 242 internal_cfg.default_usr_cfg.instance_name_pattern) 243 self.fetch_cvd_version = ( 244 usr_cfg.fetch_cvd_version or 245 internal_cfg.default_usr_cfg.fetch_cvd_version) 246 if usr_cfg.HasField("enable_multi_stage") is not None: 247 self.enable_multi_stage = usr_cfg.enable_multi_stage 248 elif internal_cfg.default_usr_cfg.HasField("enable_multi_stage"): 249 self.enable_multi_stage = internal_cfg.default_usr_cfg.enable_multi_stage 250 else: 251 self.enable_multi_stage = False 252 self.disk_type = usr_cfg.disk_type 253 254 # Verify validity of configurations. 255 self.Verify() 256 257 # pylint: disable=too-many-branches 258 def OverrideWithArgs(self, parsed_args): 259 """Override configuration values with args passed in from cmd line. 260 261 Args: 262 parsed_args: Args parsed from command line. 263 """ 264 if parsed_args.which == create_args.CMD_CREATE and parsed_args.spec: 265 if not self.resolution: 266 self.resolution = self.device_resolution_map.get( 267 parsed_args.spec, "") 268 if not self.orientation: 269 self.orientation = self.device_default_orientation_map.get( 270 parsed_args.spec, "") 271 if parsed_args.email: 272 self.service_account_name = parsed_args.email 273 if parsed_args.service_account_json_private_key_path: 274 self.service_account_json_private_key_path = ( 275 parsed_args.service_account_json_private_key_path) 276 if parsed_args.which == "create_gf" and parsed_args.base_image: 277 self.stable_goldfish_host_image_name = parsed_args.base_image 278 if parsed_args.which in [create_args.CMD_CREATE, "create_cf"]: 279 if parsed_args.network: 280 self.network = parsed_args.network 281 if parsed_args.multi_stage_launch is not None: 282 self.enable_multi_stage = parsed_args.multi_stage_launch 283 if parsed_args.which in [create_args.CMD_CREATE, "create_cf", "create_gf"]: 284 if parsed_args.zone: 285 self.zone = parsed_args.zone 286 if (parsed_args.which == "create_cf" and 287 parsed_args.num_avds_per_instance > 1): 288 scrubbed_args = [arg for arg in self.launch_args.split() 289 if _NUM_INSTANCES_ARG not in arg] 290 scrubbed_args.append("%s=%d" % (_NUM_INSTANCES_ARG, 291 parsed_args.num_avds_per_instance)) 292 293 self.launch_args = " ".join(scrubbed_args) 294 295 def GetDefaultHwProperty(self, flavor, instance_type=None): 296 """Get default hw configuration values. 297 298 HwProperty will be overrided according to the change of flavor and 299 instance type. The format of key is flavor or instance_type-flavor. 300 e.g: 'phone' or 'local-phone'. 301 If the giving key is not found, get hw configuration with a default 302 phone property. 303 304 Args: 305 flavor: String of flavor name. 306 instance_type: String of instance type. 307 308 Returns: 309 String of device hardware property, it would be like 310 "cpu:4,resolution:720x1280,dpi:320,memory:4g". 311 """ 312 hw_key = ("%s-%s" % (instance_type, flavor) 313 if instance_type == constants.INSTANCE_TYPE_LOCAL else flavor) 314 return self.common_hw_property_map.get(hw_key, _DEFAULT_HW_PROPERTY) 315 316 def Verify(self): 317 """Verify configuration fields.""" 318 missing = self.GetMissingFields(self.REQUIRED_FIELD) 319 if missing: 320 raise errors.ConfigError( 321 "Missing required configuration fields: %s" % missing) 322 if (self.extra_data_disk_size_gb and self.extra_data_disk_size_gb not in 323 self.precreated_data_image_map): 324 raise errors.ConfigError( 325 "Supported extra_data_disk_size_gb options(gb): %s, " 326 "invalid value: %d" % (self.precreated_data_image_map.keys(), 327 self.extra_data_disk_size_gb)) 328 329 def GetMissingFields(self, fields): 330 """Get missing required fields. 331 332 Args: 333 fields: List of field names. 334 335 Returns: 336 List of missing field names. 337 """ 338 return [f for f in fields if not getattr(self, f)] 339 340 def SupportRemoteInstance(self): 341 """Return True if gcp project is provided in config.""" 342 return bool(self.project) 343 344 345class AcloudConfigManager(): 346 """A class that loads configurations.""" 347 348 _DEFAULT_INTERNAL_CONFIG_PATH = os.path.join(_CONFIG_DATA_PATH, 349 "default.config") 350 351 def __init__(self, 352 user_config_path, 353 internal_config_path=_DEFAULT_INTERNAL_CONFIG_PATH): 354 """Initialize with user specified paths to configs. 355 356 Args: 357 user_config_path: path to the user config. 358 internal_config_path: path to the internal conifg. 359 """ 360 self.user_config_path = user_config_path 361 self._internal_config_path = internal_config_path 362 363 def Load(self): 364 """Load the configurations. 365 366 Load user config with some special design. 367 1. User specified user config: 368 a.User config exist: Load config. 369 b.User config didn't exist: Raise exception. 370 2. User didn't specify user config, use default config: 371 a.Default config exist: Load config. 372 b.Default config didn't exist: provide empty usr_cfg. 373 374 Raises: 375 errors.ConfigError: If config file doesn't exist. 376 377 Returns: 378 An instance of AcloudConfig. 379 """ 380 internal_cfg = None 381 usr_cfg = None 382 try: 383 with open(self._internal_config_path) as config_file: 384 internal_cfg = self.LoadConfigFromProtocolBuffer( 385 config_file, internal_config_pb2.InternalConfig) 386 except OSError as e: 387 raise errors.ConfigError("Could not load config files: %s" % str(e)) 388 # Load user config file 389 self.user_config_path = GetUserConfigPath(self.user_config_path) 390 if os.path.exists(self.user_config_path): 391 with open(self.user_config_path, "r") as config_file: 392 usr_cfg = self.LoadConfigFromProtocolBuffer( 393 config_file, user_config_pb2.UserConfig) 394 else: 395 if self.user_config_path != GetDefaultConfigFile(): 396 raise errors.ConfigError( 397 "The config file doesn't exist: %s. For reset config " 398 "information: go/acloud-googler-setup#reset-configuration" % 399 (self.user_config_path)) 400 usr_cfg = user_config_pb2.UserConfig() 401 return AcloudConfig(usr_cfg, internal_cfg) 402 403 @staticmethod 404 def LoadConfigFromProtocolBuffer(config_file, message_type): 405 """Load config from a text-based protocol buffer file. 406 407 Args: 408 config_file: A python File object. 409 message_type: A proto message class. 410 411 Returns: 412 An instance of type "message_type" populated with data 413 from the file. 414 """ 415 try: 416 config = message_type() 417 text_format.Merge(config_file.read(), config) 418 return config 419 except text_format.ParseError as e: 420 raise errors.ConfigError("Could not parse config: %s" % str(e)) 421