• 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
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