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