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