Keun Soo Yim | b293fdb | 2016-09-21 16:03:44 -0700 | [diff] [blame] | 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 | |
| 19 | Three protobuf messages are defined in |
| 20 | driver/internal/config/proto/internal_config.proto |
| 21 | driver/internal/config/proto/user_config.proto |
| 22 | |
| 23 | Internal config file User config file |
| 24 | | | |
| 25 | v v |
| 26 | InternalConfig UserConfig |
| 27 | (proto message) (proto message) |
| 28 | | | |
| 29 | | | |
| 30 | |-> AcloudConfig <-| |
| 31 | |
| 32 | At 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 | |
| 37 | TODO(fdeng): |
| 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 | |
| 46 | import logging |
| 47 | import os |
| 48 | |
| 49 | from acloud.internal.proto import internal_config_pb2 |
| 50 | from acloud.internal.proto import user_config_pb2 |
| 51 | from acloud.public import errors |
| 52 | from google.protobuf import text_format |
| 53 | _CONFIG_DATA_PATH = os.path.join( |
| 54 | os.path.dirname(os.path.abspath(__file__)), "data") |
| 55 | |
| 56 | logger = logging.getLogger(__name__) |
| 57 | |
| 58 | |
| 59 | class AcloudConfig(object): |
| 60 | """A class that holds all configurations for acloud.""" |
| 61 | |
| 62 | REQUIRED_FIELD = [ |
| 63 | "project", "zone", "machine_type", "network", "storage_bucket_name", |
| 64 | "min_machine_size", "disk_image_name", "disk_image_mime_type" |
| 65 | ] |
| 66 | |
| 67 | def __init__(self, usr_cfg, internal_cfg): |
| 68 | """Initialize. |
| 69 | |
| 70 | Args: |
| 71 | usr_cfg: A protobuf object that holds the user configurations. |
| 72 | internal_cfg: A protobuf object that holds internal configurations. |
| 73 | """ |
| 74 | self.service_account_name = usr_cfg.service_account_name |
| 75 | self.service_account_private_key_path = ( |
| 76 | usr_cfg.service_account_private_key_path) |
| 77 | self.creds_cache_file = internal_cfg.creds_cache_file |
| 78 | self.user_agent = internal_cfg.user_agent |
| 79 | self.client_id = usr_cfg.client_id |
| 80 | self.client_secret = usr_cfg.client_secret |
| 81 | |
| 82 | self.project = usr_cfg.project |
| 83 | self.zone = usr_cfg.zone |
| 84 | self.machine_type = (usr_cfg.machine_type or |
| 85 | internal_cfg.default_usr_cfg.machine_type) |
| 86 | self.network = (usr_cfg.network or |
| 87 | internal_cfg.default_usr_cfg.network) |
| 88 | self.ssh_private_key_path = usr_cfg.ssh_private_key_path |
Fang Deng | fed6a6f | 2017-03-01 18:27:28 -0800 | [diff] [blame] | 89 | self.ssh_public_key_path = usr_cfg.ssh_public_key_path |
Keun Soo Yim | b293fdb | 2016-09-21 16:03:44 -0700 | [diff] [blame] | 90 | self.storage_bucket_name = usr_cfg.storage_bucket_name |
| 91 | self.metadata_variable = { |
| 92 | key: val |
| 93 | for key, val in |
| 94 | internal_cfg.default_usr_cfg.metadata_variable.iteritems() |
| 95 | } |
| 96 | self.metadata_variable.update(usr_cfg.metadata_variable) |
| 97 | |
| 98 | self.device_resolution_map = { |
| 99 | device: resolution |
| 100 | for device, resolution in |
| 101 | internal_cfg.device_resolution_map.iteritems() |
| 102 | } |
| 103 | self.device_default_orientation_map = { |
| 104 | device: orientation |
| 105 | for device, orientation in |
| 106 | internal_cfg.device_default_orientation_map.iteritems() |
| 107 | } |
Fang Deng | cef4b11 | 2017-03-02 11:20:17 -0800 | [diff] [blame] | 108 | self.no_project_access_msg_map = { |
| 109 | project: msg for project, msg |
| 110 | in internal_cfg.no_project_access_msg_map.iteritems()} |
Keun Soo Yim | b293fdb | 2016-09-21 16:03:44 -0700 | [diff] [blame] | 111 | self.min_machine_size = internal_cfg.min_machine_size |
| 112 | self.disk_image_name = internal_cfg.disk_image_name |
| 113 | self.disk_image_mime_type = internal_cfg.disk_image_mime_type |
| 114 | self.disk_image_extension = internal_cfg.disk_image_extension |
| 115 | self.disk_raw_image_name = internal_cfg.disk_raw_image_name |
| 116 | self.disk_raw_image_extension = internal_cfg.disk_raw_image_extension |
| 117 | self.valid_branch_and_min_build_id = { |
| 118 | branch: min_build_id |
| 119 | for branch, min_build_id in |
| 120 | internal_cfg.valid_branch_and_min_build_id.iteritems() |
| 121 | } |
| 122 | self.precreated_data_image_map = { |
| 123 | size_gb: image_name |
| 124 | for size_gb, image_name in |
| 125 | internal_cfg.precreated_data_image.iteritems() |
| 126 | } |
| 127 | self.extra_data_disk_size_gb = ( |
| 128 | usr_cfg.extra_data_disk_size_gb or |
| 129 | internal_cfg.default_usr_cfg.extra_data_disk_size_gb) |
| 130 | if self.extra_data_disk_size_gb > 0: |
| 131 | if "cfg_sta_persistent_data_device" not in usr_cfg.metadata_variable: |
| 132 | # If user did not set it explicity, use default. |
| 133 | self.metadata_variable["cfg_sta_persistent_data_device"] = ( |
| 134 | internal_cfg.default_extra_data_disk_device) |
| 135 | if "cfg_sta_ephemeral_data_size_mb" in usr_cfg.metadata_variable: |
| 136 | raise errors.ConfigError( |
| 137 | "The following settings can't be set at the same time: " |
| 138 | "extra_data_disk_size_gb and" |
| 139 | "metadata variable cfg_sta_ephemeral_data_size_mb.") |
| 140 | if "cfg_sta_ephemeral_data_size_mb" in self.metadata_variable: |
| 141 | del self.metadata_variable["cfg_sta_ephemeral_data_size_mb"] |
| 142 | |
| 143 | # Fields that can be overriden by args |
| 144 | self.orientation = usr_cfg.orientation |
| 145 | self.resolution = usr_cfg.resolution |
| 146 | |
| 147 | # Verify validity of configurations. |
| 148 | self.Verify() |
| 149 | |
| 150 | def OverrideWithArgs(self, parsed_args): |
| 151 | """Override configuration values with args passed in from cmd line. |
| 152 | |
| 153 | Args: |
| 154 | parsed_args: Args parsed from command line. |
| 155 | """ |
| 156 | if parsed_args.which == "create" and parsed_args.spec: |
| 157 | if not self.resolution: |
| 158 | self.resolution = self.device_resolution_map.get( |
| 159 | parsed_args.spec, "") |
| 160 | if not self.orientation: |
| 161 | self.orientation = self.device_default_orientation_map.get( |
| 162 | parsed_args.spec, "") |
| 163 | if parsed_args.email: |
| 164 | self.service_account_name = parsed_args.email |
| 165 | |
| 166 | def Verify(self): |
| 167 | """Verify configuration fields.""" |
| 168 | missing = [f for f in self.REQUIRED_FIELD if not getattr(self, f)] |
| 169 | if missing: |
| 170 | raise errors.ConfigError( |
| 171 | "Missing required configuration fields: %s" % missing) |
| 172 | if (self.extra_data_disk_size_gb and self.extra_data_disk_size_gb |
| 173 | not in self.precreated_data_image_map): |
| 174 | raise errors.ConfigError( |
| 175 | "Supported extra_data_disk_size_gb options(gb): %s, " |
| 176 | "invalid value: %d" % (self.precreated_data_image_map.keys(), |
| 177 | self.extra_data_disk_size_gb)) |
| 178 | |
| 179 | |
| 180 | class AcloudConfigManager(object): |
| 181 | """A class that loads configurations.""" |
| 182 | |
| 183 | _DEFAULT_INTERNAL_CONFIG_PATH = os.path.join(_CONFIG_DATA_PATH, |
| 184 | "default.config") |
| 185 | |
| 186 | def __init__(self, |
| 187 | user_config_path, |
| 188 | internal_config_path=_DEFAULT_INTERNAL_CONFIG_PATH): |
| 189 | """Initialize. |
| 190 | |
| 191 | Args: |
| 192 | user_config_path: path to the user config. |
| 193 | internal_config_path: path to the internal conifg. |
| 194 | """ |
| 195 | self._user_config_path = user_config_path |
| 196 | self._internal_config_path = internal_config_path |
| 197 | |
| 198 | def Load(self): |
| 199 | """Load the configurations.""" |
| 200 | internal_cfg = None |
| 201 | usr_cfg = None |
| 202 | try: |
| 203 | with open(self._internal_config_path) as config_file: |
| 204 | internal_cfg = self.LoadConfigFromProtocolBuffer( |
| 205 | config_file, internal_config_pb2.InternalConfig) |
| 206 | |
| 207 | with open(self._user_config_path, "r") as config_file: |
| 208 | usr_cfg = self.LoadConfigFromProtocolBuffer( |
| 209 | config_file, user_config_pb2.UserConfig) |
| 210 | except OSError as e: |
| 211 | raise errors.ConfigError("Could not load config files: %s" % |
| 212 | str(e)) |
| 213 | return AcloudConfig(usr_cfg, internal_cfg) |
| 214 | |
| 215 | @staticmethod |
| 216 | def LoadConfigFromProtocolBuffer(config_file, message_type): |
| 217 | """Load config from a text-based protocol buffer file. |
| 218 | |
| 219 | Args: |
| 220 | config_file: A python File object. |
| 221 | message_type: A proto message class. |
| 222 | |
| 223 | Returns: |
| 224 | An instance of type "message_type" populated with data |
| 225 | from the file. |
| 226 | """ |
| 227 | try: |
| 228 | config = message_type() |
| 229 | text_format.Merge(config_file.read(), config) |
| 230 | return config |
| 231 | except text_format.ParseError as e: |
| 232 | raise errors.ConfigError("Could not parse config: %s" % str(e)) |