# Copyright 2019 - The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """RemoteInstanceDeviceFactory provides basic interface to create a cuttlefish device factory.""" import logging import os import shutil import subprocess import tempfile from acloud import errors from acloud.create import create_common from acloud.internal import constants from acloud.internal.lib import cvd_utils from acloud.internal.lib import utils from acloud.public.actions import gce_device_factory from acloud.pull import pull logger = logging.getLogger(__name__) class RemoteInstanceDeviceFactory(gce_device_factory.GCEDeviceFactory): """A class that can produce a cuttlefish device. Attributes: avd_spec: AVDSpec object that tells us what we're going to create. cfg: An AcloudConfig instance. local_image_artifact: A string, path to local image. cvd_host_package_artifact: A string, path to cvd host package. report_internal_ip: Boolean, True for the internal ip is used when connecting from another GCE instance. credentials: An oauth2client.OAuth2Credentials instance. compute_client: An object of cvd_compute_client.CvdComputeClient. ssh: An Ssh object. """ def __init__(self, avd_spec, local_image_artifact=None, cvd_host_package_artifact=None): super().__init__(avd_spec, local_image_artifact) self._all_logs = {} self._cvd_host_package_artifact = cvd_host_package_artifact # pylint: disable=broad-except def CreateInstance(self): """Create a single configured cuttlefish device. Returns: A string, representing instance name. """ instance = self.CreateGceInstance() # If instance is failed, no need to go next step. if instance in self.GetFailures(): return instance try: image_args = self._ProcessArtifacts() failures = self._compute_client.LaunchCvd( instance, self._avd_spec, cvd_utils.GCE_BASE_DIR, image_args) for failing_instance, error_msg in failures.items(): self._SetFailures(failing_instance, error_msg) except Exception as e: self._SetFailures(instance, e) try: self._FindLogFiles( instance, (instance in self.GetFailures() and not self._avd_spec.no_pull_log)) except (errors.SubprocessFail, errors.DeviceConnectionError, subprocess.CalledProcessError, subprocess.TimeoutExpired) as e: logger.error("Fail to find log files: %s", e) return instance def _ProcessArtifacts(self): """Process artifacts. - If images source is local, tool will upload images from local site to remote instance. - If images source is remote, tool will download images from android build to remote instance. Before download images, we have to update fetch_cvd to remote instance. Returns: A list of strings, the launch_cvd arguments. """ avd_spec = self._avd_spec launch_cvd_args = [] temp_dir = None try: target_files_dir = None if cvd_utils.AreTargetFilesRequired(avd_spec): temp_dir = tempfile.mkdtemp(prefix="acloud_remote_ins") target_files_dir = self._GetLocalTargetFilesDir(temp_dir) if avd_spec.image_source == constants.IMAGE_SRC_LOCAL: cvd_utils.UploadArtifacts( self._ssh, cvd_utils.GCE_BASE_DIR, (target_files_dir or self._local_image_artifact or avd_spec.local_image_dir), self._cvd_host_package_artifact) elif avd_spec.image_source == constants.IMAGE_SRC_REMOTE: self._compute_client.FetchBuild( avd_spec.remote_image, avd_spec.system_build_info, avd_spec.kernel_build_info, avd_spec.boot_build_info, avd_spec.bootloader_build_info, avd_spec.android_efi_loader_build_info, avd_spec.ota_build_info, avd_spec.host_package_build_info) launch_cvd_args += cvd_utils.UploadExtraImages( self._ssh, cvd_utils.GCE_BASE_DIR, avd_spec, target_files_dir) finally: if temp_dir: shutil.rmtree(temp_dir) if avd_spec.mkcert and avd_spec.connect_webrtc: self._compute_client.UpdateCertificate() if avd_spec.extra_files: self._compute_client.UploadExtraFiles(avd_spec.extra_files) return [arg for arg_pair in launch_cvd_args for arg in arg_pair] @utils.TimeExecute(function_description="Downloading target_files archive") def _DownloadTargetFiles(self, download_dir): """Download target_files zip to a directory. Args: download_dir: The directory to which the zip is downloaded. Returns: The path to the target_files zip. """ avd_spec = self._avd_spec build_id = avd_spec.remote_image[constants.BUILD_ID] build_target = avd_spec.remote_image[constants.BUILD_TARGET] name = cvd_utils.GetMixBuildTargetFilename(build_target, build_id) create_common.DownloadRemoteArtifact(avd_spec.cfg, build_target, build_id, name, download_dir) return os.path.join(download_dir, name) def _GetLocalTargetFilesDir(self, temp_dir): """Return a directory of extracted target_files or local images. Args: temp_dir: Temporary directory to store downloaded build artifacts and extracted target_files archive. Returns: The path to the target_files directory. """ avd_spec = self._avd_spec if avd_spec.image_source == constants.IMAGE_SRC_LOCAL: if self._local_image_artifact: target_files_zip = self._local_image_artifact target_files_dir = os.path.join(temp_dir, "local_images") else: return os.path.abspath(avd_spec.local_image_dir) else: # must be IMAGE_SRC_REMOTE target_files_zip = self._DownloadTargetFiles(temp_dir) target_files_dir = os.path.join(temp_dir, "remote_images") os.makedirs(target_files_dir, exist_ok=True) cvd_utils.ExtractTargetFilesZip(target_files_zip, target_files_dir) return target_files_dir def _FindLogFiles(self, instance, download): """Find and pull all log files from instance. Args: instance: String, instance name. download: Whether to download the files to a temporary directory and show messages to the user. """ logs = [cvd_utils.HOST_KERNEL_LOG] if self._avd_spec.image_source == constants.IMAGE_SRC_REMOTE: logs.append( cvd_utils.GetRemoteFetcherConfigJson(cvd_utils.GCE_BASE_DIR)) logs.extend(cvd_utils.FindRemoteLogs( self._ssh, cvd_utils.GCE_BASE_DIR, self._avd_spec.base_instance_num, self._avd_spec.num_avds_per_instance)) self._all_logs[instance] = logs if download: # To avoid long download time, fetch from the first device only. log_files = pull.GetAllLogFilePaths(self._ssh, constants.REMOTE_LOG_FOLDER) error_log_folder = pull.PullLogs(self._ssh, log_files, instance) self._compute_client.ExtendReportData(constants.ERROR_LOG_FOLDER, error_log_folder) def GetOpenWrtInfoDict(self): """Get openwrt info dictionary. Returns: A openwrt info dictionary. None for the case is not openwrt device. """ if not self._avd_spec.openwrt: return None return cvd_utils.GetOpenWrtInfoDict(self._ssh, cvd_utils.GCE_BASE_DIR) def GetAdbPorts(self): """Get ADB ports of the created devices. Returns: The port numbers as a list of integers. """ return cvd_utils.GetAdbPorts(self._avd_spec.base_instance_num, self._avd_spec.num_avds_per_instance) def GetVncPorts(self): """Get VNC ports of the created devices. Returns: The port numbers as a list of integers. """ return cvd_utils.GetVncPorts(self._avd_spec.base_instance_num, self._avd_spec.num_avds_per_instance) def GetBuildInfoDict(self): """Get build info dictionary. Returns: A build info dictionary. None for local image case. """ if self._avd_spec.image_source == constants.IMAGE_SRC_LOCAL: return None return cvd_utils.GetRemoteBuildInfoDict(self._avd_spec) def GetLogs(self): """Get all device logs. Returns: A dictionary that maps instance names to lists of report.LogFile. """ return self._all_logs