• 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_DEFAULT_HW_PROPERTY = "cpu:4,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():
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        self.betty_image = usr_cfg.betty_image
228
229        self.extra_args_ssh_tunnel = usr_cfg.extra_args_ssh_tunnel
230
231        self.common_hw_property_map = internal_cfg.common_hw_property_map
232        self.hw_property = usr_cfg.hw_property
233
234        self.launch_args = usr_cfg.launch_args
235        self.api_key = usr_cfg.api_key
236        self.api_url = usr_cfg.api_url
237        self.instance_name_pattern = (
238            usr_cfg.instance_name_pattern or
239            internal_cfg.default_usr_cfg.instance_name_pattern)
240        self.fetch_cvd_version = (
241            usr_cfg.fetch_cvd_version or
242            internal_cfg.default_usr_cfg.fetch_cvd_version)
243        if usr_cfg.HasField("enable_multi_stage") is not None:
244            self.enable_multi_stage = usr_cfg.enable_multi_stage
245        elif internal_cfg.default_usr_cfg.HasField("enable_multi_stage"):
246            self.enable_multi_stage = internal_cfg.default_usr_cfg.enable_multi_stage
247        else:
248            self.enable_multi_stage = False
249
250        # Verify validity of configurations.
251        self.Verify()
252
253    # pylint: disable=too-many-branches
254    def OverrideWithArgs(self, parsed_args):
255        """Override configuration values with args passed in from cmd line.
256
257        Args:
258            parsed_args: Args parsed from command line.
259        """
260        if parsed_args.which == create_args.CMD_CREATE and parsed_args.spec:
261            if not self.resolution:
262                self.resolution = self.device_resolution_map.get(
263                    parsed_args.spec, "")
264            if not self.orientation:
265                self.orientation = self.device_default_orientation_map.get(
266                    parsed_args.spec, "")
267        if parsed_args.email:
268            self.service_account_name = parsed_args.email
269        if parsed_args.service_account_json_private_key_path:
270            self.service_account_json_private_key_path = (
271                parsed_args.service_account_json_private_key_path)
272        if parsed_args.which == "create_gf" and parsed_args.base_image:
273            self.stable_goldfish_host_image_name = parsed_args.base_image
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 GetDefaultHwProperty(self, flavor, instance_type=None):
292        """Get default 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, get 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        Returns:
305            String of device hardware property, it would be like
306            "cpu:4,resolution:720x1280,dpi:320,memory:4g".
307        """
308        hw_key = ("%s-%s" % (instance_type, flavor)
309                  if instance_type == constants.INSTANCE_TYPE_LOCAL else flavor)
310        return self.common_hw_property_map.get(hw_key, _DEFAULT_HW_PROPERTY)
311
312    def Verify(self):
313        """Verify configuration fields."""
314        missing = self.GetMissingFields(self.REQUIRED_FIELD)
315        if missing:
316            raise errors.ConfigError(
317                "Missing required configuration fields: %s" % missing)
318        if (self.extra_data_disk_size_gb and self.extra_data_disk_size_gb not in
319                self.precreated_data_image_map):
320            raise errors.ConfigError(
321                "Supported extra_data_disk_size_gb options(gb): %s, "
322                "invalid value: %d" % (self.precreated_data_image_map.keys(),
323                                       self.extra_data_disk_size_gb))
324
325    def GetMissingFields(self, fields):
326        """Get missing required fields.
327
328        Args:
329            fields: List of field names.
330
331        Returns:
332            List of missing field names.
333        """
334        return [f for f in fields if not getattr(self, f)]
335
336    def SupportRemoteInstance(self):
337        """Return True if gcp project is provided in config."""
338        return bool(self.project)
339
340
341class AcloudConfigManager():
342    """A class that loads configurations."""
343
344    _DEFAULT_INTERNAL_CONFIG_PATH = os.path.join(_CONFIG_DATA_PATH,
345                                                 "default.config")
346
347    def __init__(self,
348                 user_config_path,
349                 internal_config_path=_DEFAULT_INTERNAL_CONFIG_PATH):
350        """Initialize with user specified paths to configs.
351
352        Args:
353            user_config_path: path to the user config.
354            internal_config_path: path to the internal conifg.
355        """
356        self.user_config_path = user_config_path
357        self._internal_config_path = internal_config_path
358
359    def Load(self):
360        """Load the configurations.
361
362        Load user config with some special design.
363        1. User specified user config:
364            a.User config exist: Load config.
365            b.User config didn't exist: Raise exception.
366        2. User didn't specify user config, use default config:
367            a.Default config exist: Load config.
368            b.Default config didn't exist: provide empty usr_cfg.
369        """
370        internal_cfg = None
371        usr_cfg = None
372        try:
373            with open(self._internal_config_path) as config_file:
374                internal_cfg = self.LoadConfigFromProtocolBuffer(
375                    config_file, internal_config_pb2.InternalConfig)
376        except OSError as e:
377            raise errors.ConfigError("Could not load config files: %s" % str(e))
378        # Load user config file
379        if self.user_config_path:
380            if os.path.exists(self.user_config_path):
381                with open(self.user_config_path, "r") as config_file:
382                    usr_cfg = self.LoadConfigFromProtocolBuffer(
383                        config_file, user_config_pb2.UserConfig)
384            else:
385                raise errors.ConfigError("The file doesn't exist: %s" %
386                                         (self.user_config_path))
387        else:
388            self.user_config_path = GetDefaultConfigFile()
389            if os.path.exists(self.user_config_path):
390                with open(self.user_config_path, "r") as config_file:
391                    usr_cfg = self.LoadConfigFromProtocolBuffer(
392                        config_file, user_config_pb2.UserConfig)
393            else:
394                usr_cfg = user_config_pb2.UserConfig()
395        return AcloudConfig(usr_cfg, internal_cfg)
396
397    @staticmethod
398    def LoadConfigFromProtocolBuffer(config_file, message_type):
399        """Load config from a text-based protocol buffer file.
400
401        Args:
402            config_file: A python File object.
403            message_type: A proto message class.
404
405        Returns:
406            An instance of type "message_type" populated with data
407            from the file.
408        """
409        try:
410            config = message_type()
411            text_format.Merge(config_file.read(), config)
412            return config
413        except text_format.ParseError as e:
414            raise errors.ConfigError("Could not parse config: %s" % str(e))
415