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