• 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        if usr_cfg.HasField("enable_multi_stage") is not None:
250            self.enable_multi_stage = usr_cfg.enable_multi_stage
251        elif internal_cfg.default_usr_cfg.HasField("enable_multi_stage"):
252            self.enable_multi_stage = internal_cfg.default_usr_cfg.enable_multi_stage
253        else:
254            self.enable_multi_stage = False
255        self.disk_type = usr_cfg.disk_type
256        self.use_cvdr = usr_cfg.use_cvdr
257        self.use_legacy_acloud = usr_cfg.use_legacy_acloud
258
259        # Verify validity of configurations.
260        self.Verify()
261
262    # pylint: disable=too-many-branches
263    def OverrideWithArgs(self, parsed_args):
264        """Override configuration values with args passed in from cmd line.
265
266        Args:
267            parsed_args: Args parsed from command line.
268        """
269        if parsed_args.which == create_args.CMD_CREATE and parsed_args.spec:
270            if not self.resolution:
271                self.resolution = self.device_resolution_map.get(
272                    parsed_args.spec, "")
273            if not self.orientation:
274                self.orientation = self.device_default_orientation_map.get(
275                    parsed_args.spec, "")
276        if parsed_args.email:
277            self.service_account_name = parsed_args.email
278        if parsed_args.service_account_json_private_key_path:
279            self.service_account_json_private_key_path = (
280                parsed_args.service_account_json_private_key_path)
281        if parsed_args.which == "create_gf" and parsed_args.base_image:
282            self.stable_goldfish_host_image_name = parsed_args.base_image
283        if parsed_args.which in [create_args.CMD_CREATE, "create_cf"]:
284            if parsed_args.network:
285                self.network = parsed_args.network
286            if parsed_args.multi_stage_launch is not None:
287                self.enable_multi_stage = parsed_args.multi_stage_launch
288        if parsed_args.which in [create_args.CMD_CREATE, "create_cf", "create_gf"]:
289            if parsed_args.zone:
290                self.zone = parsed_args.zone
291        if (parsed_args.which == "create_cf" and
292                parsed_args.num_avds_per_instance > 1):
293            scrubbed_args = [arg for arg in self.launch_args.split()
294                             if _NUM_INSTANCES_ARG not in arg]
295            scrubbed_args.append("%s=%d" % (_NUM_INSTANCES_ARG,
296                                            parsed_args.num_avds_per_instance))
297
298            self.launch_args = " ".join(scrubbed_args)
299
300    def GetDefaultHwProperty(self, flavor, instance_type=None):
301        """Get default hw configuration values.
302
303        HwProperty will be overrided according to the change of flavor and
304        instance type. The format of key is flavor or instance_type-flavor.
305        e.g: 'phone' or 'local-phone'.
306        If the giving key is not found, get hw configuration with a default
307        phone property.
308
309        Args:
310            flavor: String of flavor name.
311            instance_type: String of instance type.
312
313        Returns:
314            String of device hardware property, it would be like
315            "cpu:4,resolution:720x1280,dpi:320,memory:4g".
316        """
317        hw_key = ("%s-%s" % (instance_type, flavor)
318                  if instance_type == constants.INSTANCE_TYPE_LOCAL else flavor)
319        return self.common_hw_property_map.get(hw_key, _DEFAULT_HW_PROPERTY)
320
321    def Verify(self):
322        """Verify configuration fields."""
323        missing = self.GetMissingFields(self.REQUIRED_FIELD)
324        if missing:
325            raise errors.ConfigError(
326                "Missing required configuration fields: %s" % missing)
327        if (self.extra_data_disk_size_gb and self.extra_data_disk_size_gb not in
328                self.precreated_data_image_map):
329            raise errors.ConfigError(
330                "Supported extra_data_disk_size_gb options(gb): %s, "
331                "invalid value: %d" % (self.precreated_data_image_map.keys(),
332                                       self.extra_data_disk_size_gb))
333
334    def GetMissingFields(self, fields):
335        """Get missing required fields.
336
337        Args:
338            fields: List of field names.
339
340        Returns:
341            List of missing field names.
342        """
343        return [f for f in fields if not getattr(self, f)]
344
345    def SupportRemoteInstance(self):
346        """Return True if gcp project is provided in config."""
347        return bool(self.project)
348
349
350class AcloudConfigManager():
351    """A class that loads configurations."""
352
353    def __init__(self, user_config_path):
354        """Initialize with user specified paths to configs.
355
356        Args:
357            user_config_path: path to the user config.
358        """
359        self.user_config_path = user_config_path
360
361    def Load(self):
362        """Load the configurations.
363
364        Load user config with some special design.
365        1. User specified user config:
366            a.User config exist: Load config.
367            b.User config didn't exist: Raise exception.
368        2. User didn't specify user config, use default config:
369            a.Default config exist: Load config.
370            b.Default config didn't exist: provide empty usr_cfg.
371
372        Raises:
373            errors.ConfigError: If config file doesn't exist.
374
375        Returns:
376            An instance of AcloudConfig.
377        """
378        internal_cfg = None
379        usr_cfg = None
380        try:
381            with _OpenTextResource(_INTERNAL_CONFIG_FILE) as cfg_file:
382                internal_cfg = self.LoadConfigFromProtocolBuffer(
383                    cfg_file, internal_config_pb2.InternalConfig)
384        except OSError as e:
385            raise errors.ConfigError("Could not load config files: %s" % str(e))
386        # Load user config file
387        self.user_config_path = GetUserConfigPath(self.user_config_path)
388        if os.path.exists(self.user_config_path):
389            with open(self.user_config_path, "r") as config_file:
390                usr_cfg = self.LoadConfigFromProtocolBuffer(
391                    config_file, user_config_pb2.UserConfig)
392        else:
393            if self.user_config_path != GetDefaultConfigFile():
394                raise errors.ConfigError(
395                    "The config file doesn't exist: %s. For reset config "
396                    "information: go/acloud-googler-setup#reset-configuration" %
397                    (self.user_config_path))
398            usr_cfg = user_config_pb2.UserConfig()
399        return AcloudConfig(usr_cfg, internal_cfg)
400
401    @staticmethod
402    def LoadConfigFromProtocolBuffer(config_file, message_type):
403        """Load config from a text-based protocol buffer file.
404
405        Args:
406            config_file: A python File object.
407            message_type: A proto message class.
408
409        Returns:
410            An instance of type "message_type" populated with data
411            from the file.
412        """
413        try:
414            config = message_type()
415            text_format.Merge(config_file.read(), config)
416            return config
417        except text_format.ParseError as e:
418            raise errors.ConfigError("Could not parse config: %s" % str(e))
419