1#!/usr/bin/env python
2#
3# Copyright 2018 - 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.
16r"""RemoteImageRemoteInstance class.
17
18Create class that is responsible for creating a remote instance AVD with a
19remote image.
20"""
21
22import logging
23import re
24import subprocess
25import time
26
27from acloud.create import base_avd_create
28from acloud.internal import constants
29from acloud.internal.lib import oxygen_client
30from acloud.internal.lib import utils
31from acloud.public.actions import common_operations
32from acloud.public.actions import remote_instance_cf_device_factory
33from acloud.public import report
34
35
36logger = logging.getLogger(__name__)
37_DEVICE = "device"
38_DEVICES = "devices"
39_LAUNCH_CVD_TIME = "launch_cvd_time"
40_RE_SESSION_ID = re.compile(r".*session_id:\"(?P<session_id>[^\"]+)")
41_RE_SERVER_URL = re.compile(r".*server_url:\"(?P<server_url>[^\"]+)")
42_RE_OXYGEN_LEASE_ERROR = re.compile(
43    r".*Error received while trying to lease device: (?P<error>.*)$", re.DOTALL)
44
45
46class RemoteImageRemoteInstance(base_avd_create.BaseAVDCreate):
47    """Create class for a remote image remote instance AVD."""
48
49    @utils.TimeExecute(function_description="Total time: ",
50                       print_before_call=False, print_status=False)
51    def _CreateAVD(self, avd_spec, no_prompts):
52        """Create the AVD.
53
54        Args:
55            avd_spec: AVDSpec object that tells us what we're going to create.
56            no_prompts: Boolean, True to skip all prompts.
57
58        Returns:
59            A Report instance.
60        """
61        if avd_spec.oxygen:
62            return self._LeaseOxygenAVD(avd_spec)
63        if avd_spec.gce_only:
64            return self._CreateGceInstance(avd_spec)
65        device_factory = remote_instance_cf_device_factory.RemoteInstanceDeviceFactory(
66            avd_spec)
67        create_report = common_operations.CreateDevices(
68            "create_cf", avd_spec.cfg, device_factory, avd_spec.num,
69            report_internal_ip=avd_spec.report_internal_ip,
70            autoconnect=avd_spec.autoconnect,
71            avd_type=constants.TYPE_CF,
72            boot_timeout_secs=avd_spec.boot_timeout_secs,
73            unlock_screen=avd_spec.unlock_screen,
74            wait_for_boot=False,
75            connect_webrtc=avd_spec.connect_webrtc,
76            client_adb_port=avd_spec.client_adb_port)
77        if create_report.status == report.Status.SUCCESS:
78            if avd_spec.connect_vnc:
79                utils.LaunchVNCFromReport(create_report, avd_spec, no_prompts)
80            if avd_spec.connect_webrtc:
81                utils.LaunchBrowserFromReport(create_report)
82
83        return create_report
84
85    def _LeaseOxygenAVD(self, avd_spec):
86        """Lease the AVD from the AVD pool.
87
88        Args:
89            avd_spec: AVDSpec object that tells us what we're going to create.
90
91        Returns:
92            A Report instance.
93        """
94        timestart = time.time()
95        session_id = None
96        server_url = None
97        try:
98            response = oxygen_client.OxygenClient.LeaseDevice(
99                avd_spec.remote_image[constants.BUILD_TARGET],
100                avd_spec.remote_image[constants.BUILD_ID],
101                avd_spec.remote_image[constants.BUILD_BRANCH],
102                avd_spec.system_build_info[constants.BUILD_TARGET],
103                avd_spec.system_build_info[constants.BUILD_ID],
104                avd_spec.kernel_build_info[constants.BUILD_TARGET],
105                avd_spec.kernel_build_info[constants.BUILD_ID],
106                avd_spec.cfg.oxygen_client,
107                avd_spec.cfg.oxygen_lease_args)
108            session_id, server_url = self._GetDeviceInfoFromResponse(response)
109            execution_time = round(time.time() - timestart, 2)
110        except subprocess.CalledProcessError as e:
111            logger.error("Failed to lease device from Oxygen, error: %s",
112                e.output)
113            response = e.output
114
115        reporter = report.Report(command="create_cf")
116        if session_id and server_url:
117            reporter.SetStatus(report.Status.SUCCESS)
118            device_data = {"instance_name": session_id,
119                           "ip": server_url}
120            device_data[_LAUNCH_CVD_TIME] = execution_time
121            dict_devices = {_DEVICES: [device_data]}
122            reporter.UpdateData(dict_devices)
123        else:
124            # Try to parse client error
125            match = _RE_OXYGEN_LEASE_ERROR.match(response)
126            if match:
127                response = match.group("error").strip()
128
129            reporter.SetStatus(report.Status.FAIL)
130            reporter.SetErrorType(constants.ACLOUD_OXYGEN_LEASE_ERROR)
131            reporter.AddError(response)
132
133        return reporter
134
135    @staticmethod
136    def _GetDeviceInfoFromResponse(response):
137        """Get session id and server url from response.
138
139        Args:
140            response: String of the response from oxygen proxy client.
141                      e.g. "2021/08/02 11:28:52 session_id: "74b6b835"
142                      server_url: "0.0.0.34" port:{type:WATERFALL ..."
143
144        Returns:
145            The session id and the server url of leased device.
146        """
147        session_id = ""
148        for line in response.splitlines():
149            session_id_match = _RE_SESSION_ID.match(line)
150            if session_id_match:
151                session_id = session_id_match.group("session_id")
152                break
153
154        server_url = ""
155        for line in response.splitlines():
156            server_url_match = _RE_SERVER_URL.match(line)
157            if server_url_match:
158                server_url = server_url_match.group("server_url")
159                break
160        return session_id, server_url
161
162    @staticmethod
163    def _CreateGceInstance(avd_spec):
164        """Create the GCE instance.
165
166        Args:
167            avd_spec: AVDSpec object.
168
169        Returns:
170            A Report instance.
171        """
172        device_factory = remote_instance_cf_device_factory.RemoteInstanceDeviceFactory(
173            avd_spec)
174        instance = device_factory.CreateGceInstance()
175        compute_client = device_factory.GetComputeClient()
176        ip = compute_client.GetInstanceIP(instance)
177        reporter = report.Report(command="create_cf")
178        reporter.SetStatus(report.Status.SUCCESS)
179        device_data = {"instance_name": instance,
180                       "ip": ip.internal if avd_spec.report_internal_ip
181                       else ip.external}
182        dict_devices = {_DEVICES: [device_data]}
183        reporter.UpdateData(dict_devices)
184        return reporter
185