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