• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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