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