• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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"""LocalImageLocalInstance class.
17
18Create class that is responsible for creating a local instance AVD with a
19local image. For launching multiple local instances under the same user,
20The cuttlefish tool requires 3 variables:
21- ANDROID_HOST_OUT: To locate the launch_cvd tool.
22- HOME: To specify the temporary folder of launch_cvd.
23- CUTTLEFISH_INSTANCE: To specify the instance id.
24Acloud user must either set ANDROID_HOST_OUT or run acloud with --local-tool.
25The user can optionally specify the folder by --local-instance-dir and the
26instance id by --local-instance.
27
28The adb port and vnc port of local instance will be decided according to
29instance id. The rule of adb port will be '6520 + [instance id] - 1' and the vnc
30port will be '6444 + [instance id] - 1'.
31e.g:
32If instance id = 3 the adb port will be 6522 and vnc port will be 6446.
33
34To delete the local instance, we will call stop_cvd with the environment variable
35[CUTTLEFISH_CONFIG_FILE] which is pointing to the runtime cuttlefish json.
36
37To run this program outside of a build environment, the following setup is
38required.
39- One of the local tool directories is a decompressed cvd host package,
40  i.e., cvd-host_package.tar.gz.
41- If the instance doesn't require mixed images, the local image directory
42  should be an unzipped update package, i.e., <target>-img-<build>.zip,
43  which contains a super image.
44- If the instance requires mixing system image, the local image directory
45  should be an unzipped target files package, i.e.,
46  <target>-target_files-<build>.zip,
47  which contains misc info and images not packed into a super image.
48- If the instance requires mixing system image, one of the local tool
49  directories should be an unzipped OTA tools package, i.e., otatools.zip.
50"""
51
52import collections
53import glob
54import logging
55import os
56import subprocess
57import sys
58
59from acloud import errors
60from acloud.create import base_avd_create
61from acloud.create import create_common
62from acloud.internal import constants
63from acloud.internal.lib import ota_tools
64from acloud.internal.lib import utils
65from acloud.internal.lib.adb_tools import AdbTools
66from acloud.list import list as list_instance
67from acloud.list import instance
68from acloud.public import report
69
70
71logger = logging.getLogger(__name__)
72
73# The boot image name pattern corresponds to the use cases:
74# - In a cuttlefish build environment, ANDROID_PRODUCT_OUT conatins boot.img
75#   and boot-debug.img. The former is the default boot image. The latter is not
76#   useful for cuttlefish.
77# - In an officially released GKI (Generic Kernel Image) package, the image
78#   name is boot-<kernel version>.img.
79_BOOT_IMAGE_NAME_PATTERN = r"boot(-[\d.]+)?\.img"
80_SYSTEM_IMAGE_NAME_PATTERN = r"system\.img"
81_MISC_INFO_FILE_NAME = "misc_info.txt"
82_TARGET_FILES_IMAGES_DIR_NAME = "IMAGES"
83_TARGET_FILES_META_DIR_NAME = "META"
84_MIXED_SUPER_IMAGE_NAME = "mixed_super.img"
85_CMD_LAUNCH_CVD_ARGS = (
86    " -daemon -config=%s -run_adb_connector=%s "
87    "-system_image_dir %s -instance_dir %s "
88    "-undefok=report_anonymous_usage_stats,enable_sandbox,config "
89    "-report_anonymous_usage_stats=y "
90    "-enable_sandbox=false")
91_CMD_LAUNCH_CVD_HW_ARGS = " -cpus %s -x_res %s -y_res %s -dpi %s -memory_mb %s"
92_CMD_LAUNCH_CVD_DISK_ARGS = (" -blank_data_image_mb %s "
93                             "-data_policy always_create")
94_CMD_LAUNCH_CVD_WEBRTC_ARGS = (" -guest_enforce_security=false "
95                               "-vm_manager=crosvm "
96                               "-start_webrtc=true "
97                               "-webrtc_public_ip=%s" % constants.LOCALHOST)
98_CMD_LAUNCH_CVD_VNC_ARG = " -start_vnc_server=true"
99_CMD_LAUNCH_CVD_SUPER_IMAGE_ARG = " -super_image=%s"
100_CMD_LAUNCH_CVD_BOOT_IMAGE_ARG = " -boot_image=%s"
101
102# In accordance with the number of network interfaces in
103# /etc/init.d/cuttlefish-common
104_MAX_INSTANCE_ID = 10
105
106_INSTANCES_IN_USE_MSG = ("All instances are in use. Try resetting an instance "
107                         "by specifying --local-instance and an id between 1 "
108                         "and %d." % _MAX_INSTANCE_ID)
109_CONFIRM_RELAUNCH = ("\nCuttlefish AVD[id:%d] is already running. \n"
110                     "Enter 'y' to terminate current instance and launch a new "
111                     "instance, enter anything else to exit out[y/N]: ")
112
113# The first two fields of this named tuple are image folder and CVD host
114# package folder which are essential for local instances. The following fields
115# are optional. They are set when the AVD spec requires to mix images.
116ArtifactPaths = collections.namedtuple(
117    "ArtifactPaths",
118    ["image_dir", "host_bins", "misc_info", "ota_tools_dir", "system_image",
119     "boot_image"])
120
121
122class LocalImageLocalInstance(base_avd_create.BaseAVDCreate):
123    """Create class for a local image local instance AVD."""
124
125    @utils.TimeExecute(function_description="Total time: ",
126                       print_before_call=False, print_status=False)
127    def _CreateAVD(self, avd_spec, no_prompts):
128        """Create the AVD.
129
130        Args:
131            avd_spec: AVDSpec object that tells us what we're going to create.
132            no_prompts: Boolean, True to skip all prompts.
133
134        Returns:
135            A Report instance.
136        """
137        # Running instances on local is not supported on all OS.
138        if not utils.IsSupportedPlatform(print_warning=True):
139            result_report = report.Report(command="create")
140            result_report.SetStatus(report.Status.FAIL)
141            return result_report
142
143        artifact_paths = self.GetImageArtifactsPath(avd_spec)
144
145        try:
146            ins_id, ins_lock = self._SelectAndLockInstance(avd_spec)
147        except errors.CreateError as e:
148            result_report = report.Report(command="create")
149            result_report.AddError(str(e))
150            result_report.SetStatus(report.Status.FAIL)
151            return result_report
152
153        try:
154            if not self._CheckRunningCvd(ins_id, no_prompts):
155                # Mark as in-use so that it won't be auto-selected again.
156                ins_lock.SetInUse(True)
157                sys.exit(constants.EXIT_BY_USER)
158
159            result_report = self._CreateInstance(ins_id, artifact_paths,
160                                                 avd_spec, no_prompts)
161            # The infrastructure is able to delete the instance only if the
162            # instance name is reported. This method changes the state to
163            # in-use after creating the report.
164            ins_lock.SetInUse(True)
165            return result_report
166        finally:
167            ins_lock.Unlock()
168
169    @staticmethod
170    def _SelectAndLockInstance(avd_spec):
171        """Select an id and lock the instance.
172
173        Args:
174            avd_spec: AVDSpec for the device.
175
176        Returns:
177            The instance id and the LocalInstanceLock that is locked by this
178            process.
179
180        Raises:
181            errors.CreateError if fails to select or lock the instance.
182        """
183        if avd_spec.local_instance_id:
184            ins_id = avd_spec.local_instance_id
185            ins_lock = instance.GetLocalInstanceLock(ins_id)
186            if ins_lock.Lock():
187                return ins_id, ins_lock
188            raise errors.CreateError("Instance %d is locked by another "
189                                     "process." % ins_id)
190
191        for ins_id in range(1, _MAX_INSTANCE_ID + 1):
192            ins_lock = instance.GetLocalInstanceLock(ins_id)
193            if ins_lock.LockIfNotInUse(timeout_secs=0):
194                logger.info("Selected instance id: %d", ins_id)
195                return ins_id, ins_lock
196        raise errors.CreateError(_INSTANCES_IN_USE_MSG)
197
198    #pylint: disable=too-many-locals
199    def _CreateInstance(self, local_instance_id, artifact_paths, avd_spec,
200                        no_prompts):
201        """Create a CVD instance.
202
203        Args:
204            local_instance_id: Integer of instance id.
205            artifact_paths: ArtifactPaths object.
206            avd_spec: AVDSpec for the instance.
207            no_prompts: Boolean, True to skip all prompts.
208
209        Returns:
210            A Report instance.
211        """
212        webrtc_port = self.GetWebrtcSigServerPort(local_instance_id)
213        if avd_spec.connect_webrtc:
214            utils.ReleasePort(webrtc_port)
215
216        cvd_home_dir = instance.GetLocalInstanceHomeDir(local_instance_id)
217        create_common.PrepareLocalInstanceDir(cvd_home_dir, avd_spec)
218        super_image_path = None
219        if artifact_paths.system_image:
220            super_image_path = self._MixSuperImage(cvd_home_dir,
221                                                   artifact_paths)
222        runtime_dir = instance.GetLocalInstanceRuntimeDir(local_instance_id)
223        # TODO(b/168171781): cvd_status of list/delete via the symbolic.
224        self.PrepareLocalCvdToolsLink(cvd_home_dir, artifact_paths.host_bins)
225        launch_cvd_path = os.path.join(artifact_paths.host_bins, "bin",
226                                       constants.CMD_LAUNCH_CVD)
227        hw_property = None
228        if avd_spec.hw_customize:
229            hw_property = avd_spec.hw_property
230        cmd = self.PrepareLaunchCVDCmd(launch_cvd_path,
231                                       hw_property,
232                                       avd_spec.connect_adb,
233                                       artifact_paths.image_dir,
234                                       runtime_dir,
235                                       avd_spec.connect_webrtc,
236                                       avd_spec.connect_vnc,
237                                       super_image_path,
238                                       artifact_paths.boot_image,
239                                       avd_spec.launch_args,
240                                       avd_spec.flavor)
241
242        result_report = report.Report(command="create")
243        instance_name = instance.GetLocalInstanceName(local_instance_id)
244        try:
245            self._LaunchCvd(cmd, local_instance_id, artifact_paths.host_bins,
246                            cvd_home_dir, (avd_spec.boot_timeout_secs or
247                                           constants.DEFAULT_CF_BOOT_TIMEOUT))
248        except errors.LaunchCVDFail as launch_error:
249            err_msg = ("Cannot create cuttlefish instance: %s\n"
250                       "For more detail: %s/launcher.log" %
251                       (launch_error, runtime_dir))
252            result_report.SetStatus(report.Status.BOOT_FAIL)
253            result_report.AddDeviceBootFailure(
254                instance_name, constants.LOCALHOST, None, None, error=err_msg)
255            return result_report
256
257        active_ins = list_instance.GetActiveCVD(local_instance_id)
258        if active_ins:
259            result_report.SetStatus(report.Status.SUCCESS)
260            result_report.AddDevice(instance_name, constants.LOCALHOST,
261                                    active_ins.adb_port, active_ins.vnc_port,
262                                    webrtc_port)
263            # Launch vnc client if we're auto-connecting.
264            if avd_spec.connect_vnc:
265                utils.LaunchVNCFromReport(result_report, avd_spec, no_prompts)
266            if avd_spec.connect_webrtc:
267                utils.LaunchBrowserFromReport(result_report)
268            if avd_spec.unlock_screen:
269                AdbTools(active_ins.adb_port).AutoUnlockScreen()
270        else:
271            err_msg = "cvd_status return non-zero after launch_cvd"
272            logger.error(err_msg)
273            result_report.SetStatus(report.Status.BOOT_FAIL)
274            result_report.AddDeviceBootFailure(
275                instance_name, constants.LOCALHOST, None, None, error=err_msg)
276        return result_report
277
278    @staticmethod
279    def GetWebrtcSigServerPort(instance_id):
280        """Get the port of the signaling server.
281
282        Args:
283            instance_id: Integer of instance id.
284
285        Returns:
286            Integer of signaling server port.
287        """
288        return constants.WEBRTC_LOCAL_PORT + instance_id - 1
289
290    @staticmethod
291    def _FindCvdHostBinaries(search_paths):
292        """Return the directory that contains CVD host binaries."""
293        for search_path in search_paths:
294            if os.path.isfile(os.path.join(search_path, "bin",
295                                           constants.CMD_LAUNCH_CVD)):
296                return search_path
297
298        for env_host_out in [constants.ENV_ANDROID_SOONG_HOST_OUT,
299                             constants.ENV_ANDROID_HOST_OUT]:
300            host_out_dir = os.environ.get(env_host_out)
301            if (host_out_dir and
302                    os.path.isfile(os.path.join(host_out_dir, "bin",
303                                                constants.CMD_LAUNCH_CVD))):
304                return host_out_dir
305
306        raise errors.GetCvdLocalHostPackageError(
307            "CVD host binaries are not found. Please run `make hosttar`, or "
308            "set --local-tool to an extracted CVD host package.")
309
310    @staticmethod
311    def _FindMiscInfo(image_dir):
312        """Find misc info in build output dir or extracted target files.
313
314        Args:
315            image_dir: The directory to search for misc info.
316
317        Returns:
318            image_dir if the directory structure looks like an output directory
319            in build environment.
320            image_dir/META if it looks like extracted target files.
321
322        Raises:
323            errors.CheckPathError if this method cannot find misc info.
324        """
325        misc_info_path = os.path.join(image_dir, _MISC_INFO_FILE_NAME)
326        if os.path.isfile(misc_info_path):
327            return misc_info_path
328        misc_info_path = os.path.join(image_dir, _TARGET_FILES_META_DIR_NAME,
329                                      _MISC_INFO_FILE_NAME)
330        if os.path.isfile(misc_info_path):
331            return misc_info_path
332        raise errors.CheckPathError(
333            "Cannot find %s in %s." % (_MISC_INFO_FILE_NAME, image_dir))
334
335    @staticmethod
336    def _FindImageDir(image_dir):
337        """Find images in build output dir or extracted target files.
338
339        Args:
340            image_dir: The directory to search for images.
341
342        Returns:
343            image_dir if the directory structure looks like an output directory
344            in build environment.
345            image_dir/IMAGES if it looks like extracted target files.
346
347        Raises:
348            errors.GetLocalImageError if this method cannot find images.
349        """
350        if glob.glob(os.path.join(image_dir, "*.img")):
351            return image_dir
352        subdir = os.path.join(image_dir, _TARGET_FILES_IMAGES_DIR_NAME)
353        if glob.glob(os.path.join(subdir, "*.img")):
354            return subdir
355        raise errors.GetLocalImageError(
356            "Cannot find images in %s." % image_dir)
357
358    def GetImageArtifactsPath(self, avd_spec):
359        """Get image artifacts path.
360
361        This method will check if launch_cvd is exist and return the tuple path
362        (image path and host bins path) where they are located respectively.
363        For remote image, RemoteImageLocalInstance will override this method and
364        return the artifacts path which is extracted and downloaded from remote.
365
366        Args:
367            avd_spec: AVDSpec object that tells us what we're going to create.
368
369        Returns:
370            ArtifactPaths object consisting of image directory and host bins
371            package.
372
373        Raises:
374            errors.GetCvdLocalHostPackageError, errors.GetLocalImageError, or
375            errors.CheckPathError if any artifact is not found.
376        """
377        image_dir = os.path.abspath(avd_spec.local_image_dir)
378        host_bins_path = self._FindCvdHostBinaries(avd_spec.local_tool_dirs)
379
380        if avd_spec.local_system_image:
381            misc_info_path = self._FindMiscInfo(image_dir)
382            image_dir = self._FindImageDir(image_dir)
383            ota_tools_dir = os.path.abspath(
384                ota_tools.FindOtaTools(avd_spec.local_tool_dirs))
385            system_image_path = create_common.FindLocalImage(
386                avd_spec.local_system_image, _SYSTEM_IMAGE_NAME_PATTERN)
387        else:
388            misc_info_path = None
389            ota_tools_dir = None
390            system_image_path = None
391
392        if avd_spec.local_kernel_image:
393            boot_image_path = create_common.FindLocalImage(
394                avd_spec.local_kernel_image, _BOOT_IMAGE_NAME_PATTERN)
395        else:
396            boot_image_path = None
397
398        return ArtifactPaths(image_dir, host_bins_path,
399                             misc_info=misc_info_path,
400                             ota_tools_dir=ota_tools_dir,
401                             system_image=system_image_path,
402                             boot_image=boot_image_path)
403
404    @staticmethod
405    def _MixSuperImage(output_dir, artifact_paths):
406        """Mix cuttlefish images and a system image into a super image.
407
408        Args:
409            output_dir: The path to the output directory.
410            artifact_paths: ArtifactPaths object.
411
412        Returns:
413            The path to the super image in output_dir.
414        """
415        ota = ota_tools.OtaTools(artifact_paths.ota_tools_dir)
416        super_image_path = os.path.join(output_dir, _MIXED_SUPER_IMAGE_NAME)
417        ota.BuildSuperImage(
418            super_image_path, artifact_paths.misc_info,
419            lambda partition: ota_tools.GetImageForPartition(
420                partition, artifact_paths.image_dir,
421                system=artifact_paths.system_image))
422        return super_image_path
423
424    @staticmethod
425    def PrepareLaunchCVDCmd(launch_cvd_path, hw_property, connect_adb,
426                            image_dir, runtime_dir, connect_webrtc,
427                            connect_vnc, super_image_path, boot_image_path,
428                            launch_args, flavor):
429        """Prepare launch_cvd command.
430
431        Create the launch_cvd commands with all the required args and add
432        in the user groups to it if necessary.
433
434        Args:
435            launch_cvd_path: String of launch_cvd path.
436            hw_property: dict object of hw property.
437            image_dir: String of local images path.
438            connect_adb: Boolean flag that enables adb_connector.
439            runtime_dir: String of runtime directory path.
440            connect_webrtc: Boolean of connect_webrtc.
441            connect_vnc: Boolean of connect_vnc.
442            super_image_path: String of non-default super image path.
443            boot_image_path: String of non-default boot image path.
444            launch_args: String of launch args.
445            flavor: String of flavor name.
446
447        Returns:
448            String, launch_cvd cmd.
449        """
450        launch_cvd_w_args = launch_cvd_path + _CMD_LAUNCH_CVD_ARGS % (
451            flavor, ("true" if connect_adb else "false"), image_dir, runtime_dir)
452        if hw_property:
453            launch_cvd_w_args = launch_cvd_w_args + _CMD_LAUNCH_CVD_HW_ARGS % (
454                hw_property["cpu"], hw_property["x_res"], hw_property["y_res"],
455                hw_property["dpi"], hw_property["memory"])
456            if constants.HW_ALIAS_DISK in hw_property:
457                launch_cvd_w_args = (launch_cvd_w_args + _CMD_LAUNCH_CVD_DISK_ARGS %
458                                     hw_property[constants.HW_ALIAS_DISK])
459        if connect_webrtc:
460            launch_cvd_w_args = launch_cvd_w_args + _CMD_LAUNCH_CVD_WEBRTC_ARGS
461
462        if connect_vnc:
463            launch_cvd_w_args = launch_cvd_w_args + _CMD_LAUNCH_CVD_VNC_ARG
464
465        if super_image_path:
466            launch_cvd_w_args = (launch_cvd_w_args +
467                                 _CMD_LAUNCH_CVD_SUPER_IMAGE_ARG %
468                                 super_image_path)
469
470        if boot_image_path:
471            launch_cvd_w_args = (launch_cvd_w_args +
472                                 _CMD_LAUNCH_CVD_BOOT_IMAGE_ARG %
473                                 boot_image_path)
474
475        if launch_args:
476            launch_cvd_w_args = launch_cvd_w_args + " " + launch_args
477
478        launch_cmd = utils.AddUserGroupsToCmd(launch_cvd_w_args,
479                                              constants.LIST_CF_USER_GROUPS)
480        logger.debug("launch_cvd cmd:\n %s", launch_cmd)
481        return launch_cmd
482
483    @staticmethod
484    def PrepareLocalCvdToolsLink(cvd_home_dir, host_bins_path):
485        """Create symbolic link for the cvd tools directory.
486
487        local instance's cvd tools could be generated in /out after local build
488        or be generated in the download image folder. It creates a symbolic
489        link then only check cvd_status using known link for both cases.
490
491        Args:
492            cvd_home_dir: The parent directory of the link
493            host_bins_path: String of host package directory.
494
495        Returns:
496            String of cvd_tools link path
497        """
498        cvd_tools_link_path = os.path.join(cvd_home_dir, constants.CVD_TOOLS_LINK_NAME)
499        if os.path.islink(cvd_tools_link_path):
500            os.unlink(cvd_tools_link_path)
501        os.symlink(host_bins_path, cvd_tools_link_path)
502        return cvd_tools_link_path
503
504    @staticmethod
505    def _CheckRunningCvd(local_instance_id, no_prompts=False):
506        """Check if launch_cvd with the same instance id is running.
507
508        Args:
509            local_instance_id: Integer of instance id.
510            no_prompts: Boolean, True to skip all prompts.
511
512        Returns:
513            Whether the user wants to continue.
514        """
515        # Check if the instance with same id is running.
516        existing_ins = list_instance.GetActiveCVD(local_instance_id)
517        if existing_ins:
518            if no_prompts or utils.GetUserAnswerYes(_CONFIRM_RELAUNCH %
519                                                    local_instance_id):
520                existing_ins.Delete()
521            else:
522                return False
523        return True
524
525    @staticmethod
526    @utils.TimeExecute(function_description="Waiting for AVD(s) to boot up")
527    def _LaunchCvd(cmd, local_instance_id, host_bins_path, cvd_home_dir,
528                   timeout):
529        """Execute Launch CVD.
530
531        Kick off the launch_cvd command and log the output.
532
533        Args:
534            cmd: String, launch_cvd command.
535            local_instance_id: Integer of instance id.
536            host_bins_path: String of host package directory.
537            cvd_home_dir: String, the home directory for the instance.
538            timeout: Integer, the number of seconds to wait for the AVD to boot up.
539
540        Raises:
541            errors.LaunchCVDFail if launch_cvd times out or returns non-zero.
542        """
543        cvd_env = os.environ.copy()
544        # launch_cvd assumes host bins are in $ANDROID_HOST_OUT.
545        cvd_env[constants.ENV_ANDROID_SOONG_HOST_OUT] = host_bins_path
546        cvd_env[constants.ENV_ANDROID_HOST_OUT] = host_bins_path
547        cvd_env[constants.ENV_CVD_HOME] = cvd_home_dir
548        cvd_env[constants.ENV_CUTTLEFISH_INSTANCE] = str(local_instance_id)
549        # Check the result of launch_cvd command.
550        # An exit code of 0 is equivalent to VIRTUAL_DEVICE_BOOT_COMPLETED
551        try:
552            subprocess.check_call(cmd, shell=True, stderr=subprocess.STDOUT,
553                                  env=cvd_env, timeout=timeout)
554        except subprocess.TimeoutExpired as e:
555            raise errors.LaunchCVDFail("Device did not boot within %d secs." %
556                                       timeout) from e
557        except subprocess.CalledProcessError as e:
558            raise errors.LaunchCVDFail("launch_cvd returned %s." %
559                                       e.returncode) from e
560