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