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 67 68def GetDefaultConfigFile(): 69 """Return path to default config file.""" 70 config_path = os.path.join(os.path.expanduser("~"), ".config", "acloud") 71 # Create the default config dir if it doesn't exist. 72 if not os.path.exists(config_path): 73 os.makedirs(config_path) 74 return os.path.join(config_path, _DEFAULT_CONFIG_FILE) 75 76 77def GetAcloudConfig(args): 78 """Helper function to initialize Config object. 79 80 Args: 81 args: Namespace object from argparse.parse_args. 82 83 Return: 84 An instance of AcloudConfig. 85 """ 86 config_mgr = AcloudConfigManager(args.config_file) 87 cfg = config_mgr.Load() 88 cfg.OverrideWithArgs(args) 89 return cfg 90 91 92class AcloudConfig(object): 93 """A class that holds all configurations for acloud.""" 94 95 REQUIRED_FIELD = [ 96 "machine_type", "network", "min_machine_size", 97 "disk_image_name", "disk_image_mime_type" 98 ] 99 100 # pylint: disable=too-many-statements 101 def __init__(self, usr_cfg, internal_cfg): 102 """Initialize. 103 104 Args: 105 usr_cfg: A protobuf object that holds the user configurations. 106 internal_cfg: A protobuf object that holds internal configurations. 107 """ 108 self.service_account_name = usr_cfg.service_account_name 109 # pylint: disable=invalid-name 110 self.service_account_private_key_path = ( 111 usr_cfg.service_account_private_key_path) 112 self.service_account_json_private_key_path = ( 113 usr_cfg.service_account_json_private_key_path) 114 self.creds_cache_file = internal_cfg.creds_cache_file 115 self.user_agent = internal_cfg.user_agent 116 self.client_id = usr_cfg.client_id 117 self.client_secret = usr_cfg.client_secret 118 119 self.project = usr_cfg.project 120 self.zone = usr_cfg.zone 121 self.machine_type = (usr_cfg.machine_type or 122 internal_cfg.default_usr_cfg.machine_type) 123 self.network = usr_cfg.network or internal_cfg.default_usr_cfg.network 124 self.ssh_private_key_path = usr_cfg.ssh_private_key_path 125 self.ssh_public_key_path = usr_cfg.ssh_public_key_path 126 self.storage_bucket_name = usr_cfg.storage_bucket_name 127 self.metadata_variable = { 128 key: val for key, val in 129 six.iteritems(internal_cfg.default_usr_cfg.metadata_variable) 130 } 131 self.metadata_variable.update(usr_cfg.metadata_variable) 132 133 self.device_resolution_map = { 134 device: resolution for device, resolution in 135 six.iteritems(internal_cfg.device_resolution_map) 136 } 137 self.device_default_orientation_map = { 138 device: orientation for device, orientation in 139 six.iteritems(internal_cfg.device_default_orientation_map) 140 } 141 self.no_project_access_msg_map = { 142 project: msg for project, msg in 143 six.iteritems(internal_cfg.no_project_access_msg_map) 144 } 145 self.min_machine_size = internal_cfg.min_machine_size 146 self.disk_image_name = internal_cfg.disk_image_name 147 self.disk_image_mime_type = internal_cfg.disk_image_mime_type 148 self.disk_image_extension = internal_cfg.disk_image_extension 149 self.disk_raw_image_name = internal_cfg.disk_raw_image_name 150 self.disk_raw_image_extension = internal_cfg.disk_raw_image_extension 151 self.valid_branch_and_min_build_id = { 152 branch: min_build_id for branch, min_build_id in 153 six.iteritems(internal_cfg.valid_branch_and_min_build_id) 154 } 155 self.precreated_data_image_map = { 156 size_gb: image_name for size_gb, image_name in 157 six.iteritems(internal_cfg.precreated_data_image) 158 } 159 self.extra_data_disk_size_gb = ( 160 usr_cfg.extra_data_disk_size_gb or 161 internal_cfg.default_usr_cfg.extra_data_disk_size_gb) 162 if self.extra_data_disk_size_gb > 0: 163 if "cfg_sta_persistent_data_device" not in usr_cfg.metadata_variable: 164 # If user did not set it explicity, use default. 165 self.metadata_variable["cfg_sta_persistent_data_device"] = ( 166 internal_cfg.default_extra_data_disk_device) 167 if "cfg_sta_ephemeral_data_size_mb" in usr_cfg.metadata_variable: 168 raise errors.ConfigError( 169 "The following settings can't be set at the same time: " 170 "extra_data_disk_size_gb and" 171 "metadata variable cfg_sta_ephemeral_data_size_mb.") 172 if "cfg_sta_ephemeral_data_size_mb" in self.metadata_variable: 173 del self.metadata_variable["cfg_sta_ephemeral_data_size_mb"] 174 175 # Additional scopes to be passed to the created instance 176 self.extra_scopes = usr_cfg.extra_scopes 177 178 # Fields that can be overriden by args 179 self.orientation = usr_cfg.orientation 180 self.resolution = usr_cfg.resolution 181 182 self.stable_host_image_name = ( 183 usr_cfg.stable_host_image_name or 184 internal_cfg.default_usr_cfg.stable_host_image_name) 185 self.stable_host_image_project = ( 186 usr_cfg.stable_host_image_project or 187 internal_cfg.default_usr_cfg.stable_host_image_project) 188 self.kernel_build_target = internal_cfg.kernel_build_target 189 190 self.emulator_build_target = internal_cfg.emulator_build_target 191 self.stable_goldfish_host_image_name = ( 192 usr_cfg.stable_goldfish_host_image_name or 193 internal_cfg.default_usr_cfg.stable_goldfish_host_image_name) 194 self.stable_goldfish_host_image_project = ( 195 usr_cfg.stable_goldfish_host_image_project or 196 internal_cfg.default_usr_cfg.stable_goldfish_host_image_project) 197 198 self.stable_cheeps_host_image_name = ( 199 usr_cfg.stable_cheeps_host_image_name or 200 internal_cfg.default_usr_cfg.stable_cheeps_host_image_name) 201 self.stable_cheeps_host_image_project = ( 202 usr_cfg.stable_cheeps_host_image_project or 203 internal_cfg.default_usr_cfg.stable_cheeps_host_image_project) 204 205 self.extra_args_ssh_tunnel = usr_cfg.extra_args_ssh_tunnel 206 207 self.common_hw_property_map = internal_cfg.common_hw_property_map 208 self.hw_property = usr_cfg.hw_property 209 210 self.launch_args = usr_cfg.launch_args 211 self.instance_name_pattern = ( 212 usr_cfg.instance_name_pattern or 213 internal_cfg.default_usr_cfg.instance_name_pattern) 214 self.fetch_cvd_version = ( 215 usr_cfg.fetch_cvd_version or 216 internal_cfg.default_usr_cfg.fetch_cvd_version) 217 if usr_cfg.HasField("enable_multi_stage") is not None: 218 self.enable_multi_stage = usr_cfg.enable_multi_stage 219 elif internal_cfg.default_usr_cfg.HasField("enable_multi_stage"): 220 self.enable_multi_stage = internal_cfg.default_usr_cfg.enable_multi_stage 221 else: 222 self.enable_multi_stage = False 223 224 # Verify validity of configurations. 225 self.Verify() 226 227 def OverrideWithArgs(self, parsed_args): 228 """Override configuration values with args passed in from cmd line. 229 230 Args: 231 parsed_args: Args parsed from command line. 232 """ 233 if parsed_args.which == create_args.CMD_CREATE and parsed_args.spec: 234 if not self.resolution: 235 self.resolution = self.device_resolution_map.get( 236 parsed_args.spec, "") 237 if not self.orientation: 238 self.orientation = self.device_default_orientation_map.get( 239 parsed_args.spec, "") 240 if parsed_args.email: 241 self.service_account_name = parsed_args.email 242 if parsed_args.service_account_json_private_key_path: 243 self.service_account_json_private_key_path = ( 244 parsed_args.service_account_json_private_key_path) 245 if parsed_args.which == "create_gf" and parsed_args.base_image: 246 self.stable_goldfish_host_image_name = parsed_args.base_image 247 if parsed_args.which == create_args.CMD_CREATE and not self.hw_property: 248 flavor = parsed_args.flavor or constants.FLAVOR_PHONE 249 self.hw_property = self.common_hw_property_map.get(flavor, "") 250 if parsed_args.which in [create_args.CMD_CREATE, "create_cf"]: 251 if parsed_args.network: 252 self.network = parsed_args.network 253 if parsed_args.multi_stage_launch is not None: 254 self.enable_multi_stage = parsed_args.multi_stage_launch 255 256 def OverrideHwPropertyWithFlavor(self, flavor): 257 """Override hw configuration values with flavor name. 258 259 HwProperty will be overrided according to the change of flavor. 260 If flavor is None, set hw configuration with phone(default flavor). 261 262 Args: 263 flavor: string of flavor name. 264 """ 265 self.hw_property = self.common_hw_property_map.get( 266 flavor, constants.FLAVOR_PHONE) 267 268 def Verify(self): 269 """Verify configuration fields.""" 270 missing = [f for f in self.REQUIRED_FIELD if not getattr(self, f)] 271 if missing: 272 raise errors.ConfigError( 273 "Missing required configuration fields: %s" % missing) 274 if (self.extra_data_disk_size_gb and self.extra_data_disk_size_gb not in 275 self.precreated_data_image_map): 276 raise errors.ConfigError( 277 "Supported extra_data_disk_size_gb options(gb): %s, " 278 "invalid value: %d" % (self.precreated_data_image_map.keys(), 279 self.extra_data_disk_size_gb)) 280 281 def SupportRemoteInstance(self): 282 """Return True if gcp project is provided in config.""" 283 return True if self.project else False 284 285 286class AcloudConfigManager(object): 287 """A class that loads configurations.""" 288 289 _DEFAULT_INTERNAL_CONFIG_PATH = os.path.join(_CONFIG_DATA_PATH, 290 "default.config") 291 292 def __init__(self, 293 user_config_path, 294 internal_config_path=_DEFAULT_INTERNAL_CONFIG_PATH): 295 """Initialize with user specified paths to configs. 296 297 Args: 298 user_config_path: path to the user config. 299 internal_config_path: path to the internal conifg. 300 """ 301 self.user_config_path = user_config_path 302 self._internal_config_path = internal_config_path 303 304 def Load(self): 305 """Load the configurations. 306 307 Load user config with some special design. 308 1. User specified user config: 309 a.User config exist: Load config. 310 b.User config didn't exist: Raise exception. 311 2. User didn't specify user config, use default config: 312 a.Default config exist: Load config. 313 b.Default config didn't exist: provide empty usr_cfg. 314 """ 315 internal_cfg = None 316 usr_cfg = None 317 try: 318 with open(self._internal_config_path) as config_file: 319 internal_cfg = self.LoadConfigFromProtocolBuffer( 320 config_file, internal_config_pb2.InternalConfig) 321 except OSError as e: 322 raise errors.ConfigError("Could not load config files: %s" % str(e)) 323 # Load user config file 324 if self.user_config_path: 325 if os.path.exists(self.user_config_path): 326 with open(self.user_config_path, "r") as config_file: 327 usr_cfg = self.LoadConfigFromProtocolBuffer( 328 config_file, user_config_pb2.UserConfig) 329 else: 330 raise errors.ConfigError("The file doesn't exist: %s" % 331 (self.user_config_path)) 332 else: 333 self.user_config_path = GetDefaultConfigFile() 334 if os.path.exists(self.user_config_path): 335 with open(self.user_config_path, "r") as config_file: 336 usr_cfg = self.LoadConfigFromProtocolBuffer( 337 config_file, user_config_pb2.UserConfig) 338 else: 339 usr_cfg = user_config_pb2.UserConfig() 340 return AcloudConfig(usr_cfg, internal_cfg) 341 342 @staticmethod 343 def LoadConfigFromProtocolBuffer(config_file, message_type): 344 """Load config from a text-based protocol buffer file. 345 346 Args: 347 config_file: A python File object. 348 message_type: A proto message class. 349 350 Returns: 351 An instance of type "message_type" populated with data 352 from the file. 353 """ 354 try: 355 config = message_type() 356 text_format.Merge(config_file.read(), config) 357 return config 358 except text_format.ParseError as e: 359 raise errors.ConfigError("Could not parse config: %s" % str(e)) 360