| #!/usr/bin/env python |
| # |
| # Copyright 2018 - 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. |
| r"""LocalImageRemoteInstance class. |
| |
| Create class that is responsible for creating a remote instance AVD with a |
| local image. |
| """ |
| |
| from distutils.spawn import find_executable |
| import getpass |
| import logging |
| import os |
| import subprocess |
| |
| from acloud import errors |
| from acloud.create import base_avd_create |
| from acloud.internal import constants |
| from acloud.internal.lib import auth |
| from acloud.internal.lib import cvd_compute_client |
| from acloud.internal.lib import utils |
| from acloud.public.actions import base_device_factory |
| from acloud.public.actions import common_operations |
| |
| logger = logging.getLogger(__name__) |
| |
| _CVD_HOST_PACKAGE = "cvd-host_package.tar.gz" |
| _CVD_USER = getpass.getuser() |
| _CMD_LAUNCH_CVD_ARGS = (" -cpus %s -x_res %s -y_res %s -dpi %s " |
| "-memory_mb %s -blank_data_image_mb %s " |
| "-data_policy always_create ") |
| |
| #Output to Serial port 1 (console) group in the instance |
| _OUTPUT_CONSOLE_GROUPS = "tty" |
| SSH_BIN = "ssh" |
| _SSH_CMD = (" -i %(rsa_key_file)s " |
| "-q -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no " |
| "-l %(login_user)s %(ip_addr)s ") |
| _SSH_CMD_MAX_RETRY = 2 |
| _SSH_CMD_RETRY_SLEEP = 3 |
| _USER_BUILD = "userbuild" |
| |
| class RemoteInstanceDeviceFactory(base_device_factory.BaseDeviceFactory): |
| """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. |
| image_path: A string, upload image artifact to instance. |
| cvd_host_package: A string, upload host package artifact to instance. |
| credentials: An oauth2client.OAuth2Credentials instance. |
| compute_client: An object of cvd_compute_client.CvdComputeClient. |
| """ |
| def __init__(self, avd_spec, local_image_artifact, cvd_host_package_artifact): |
| """Constructs a new remote instance device factory.""" |
| self._avd_spec = avd_spec |
| self._cfg = avd_spec.cfg |
| self._local_image_artifact = local_image_artifact |
| self._cvd_host_package_artifact = cvd_host_package_artifact |
| self._report_internal_ip = avd_spec.report_internal_ip |
| self.credentials = auth.CreateCredentials(avd_spec.cfg) |
| compute_client = cvd_compute_client.CvdComputeClient( |
| avd_spec.cfg, self.credentials) |
| super(RemoteInstanceDeviceFactory, self).__init__(compute_client) |
| # Private creation parameters |
| self._ssh_cmd = None |
| |
| def CreateInstance(self): |
| """Create a single configured cuttlefish device. |
| |
| 1. Create gcp instance. |
| 2. setup the AVD env in the instance. |
| 3. upload the artifacts to instance. |
| 4. Launch CVD. |
| |
| Returns: |
| A string, representing instance name. |
| """ |
| instance = self._CreateGceInstance() |
| self._SetAVDenv(_CVD_USER) |
| self._UploadArtifacts(_CVD_USER, |
| self._local_image_artifact, |
| self._cvd_host_package_artifact) |
| self._LaunchCvd(_CVD_USER, self._avd_spec.hw_property) |
| return instance |
| |
| @staticmethod |
| def _ShellCmdWithRetry(remote_cmd): |
| """Runs a shell command on remote device. |
| |
| If the network is unstable and causes SSH connect fail, it will retry. |
| When it retry in a short time, you may encounter unstable network. We |
| will use the mechanism of RETRY_BACKOFF_FACTOR. The retry time for each |
| failure is times * retries. |
| |
| Args: |
| remote_cmd: A string, shell command to be run on remote. |
| |
| Raises: |
| subprocess.CalledProcessError: For any non-zero return code of |
| remote_cmd. |
| |
| Returns: |
| Boolean, True if the command was successfully executed. False otherwise. |
| """ |
| return utils.RetryExceptionType( |
| exception_types=subprocess.CalledProcessError, |
| max_retries=_SSH_CMD_MAX_RETRY, |
| functor=lambda cmd: subprocess.check_call(cmd, shell=True), |
| sleep_multiplier=_SSH_CMD_RETRY_SLEEP, |
| retry_backoff_factor=utils.DEFAULT_RETRY_BACKOFF_FACTOR, |
| cmd=remote_cmd) |
| |
| def _CreateGceInstance(self): |
| """Create a single configured cuttlefish device. |
| |
| Override method from parent class. |
| build_target: The format is like "aosp_cf_x86_phone". We only get info |
| from the user build image file name. If the file name is |
| not custom format (no "-"), We will use the original |
| flavor as our build_target. |
| |
| Returns: |
| A string, representing instance name. |
| """ |
| image_name = os.path.basename(self._local_image_artifact) |
| build_target = self._avd_spec.flavor if "-" not in image_name else image_name.split( |
| "-")[0] |
| instance = self._compute_client.GenerateInstanceName( |
| build_target=build_target, build_id=_USER_BUILD) |
| # Create an instance from Stable Host Image |
| self._compute_client.CreateInstance( |
| instance=instance, |
| image_name=self._cfg.stable_host_image_name, |
| image_project=self._cfg.stable_host_image_project, |
| blank_data_disk_size_gb=self._cfg.extra_data_disk_size_gb, |
| avd_spec=self._avd_spec) |
| ip = self._compute_client.GetInstanceIP(instance) |
| self._ssh_cmd = find_executable(SSH_BIN) + _SSH_CMD % { |
| "login_user": getpass.getuser(), |
| "rsa_key_file": self._cfg.ssh_private_key_path, |
| "ip_addr": (ip.internal if self._report_internal_ip |
| else ip.external)} |
| return instance |
| |
| @utils.TimeExecute(function_description="Setup GCE environment") |
| def _SetAVDenv(self, cvd_user): |
| """set the user to run AVD in the instance. |
| |
| Args: |
| cvd_user: A string, user run the cvd in the instance. |
| """ |
| avd_list_of_groups = [] |
| avd_list_of_groups.extend(constants.LIST_CF_USER_GROUPS) |
| avd_list_of_groups.append(_OUTPUT_CONSOLE_GROUPS) |
| remote_cmd = "" |
| for group in avd_list_of_groups: |
| remote_cmd += "\"sudo usermod -aG %s %s;\"" %(group, cvd_user) |
| logger.debug("remote_cmd:\n %s", remote_cmd) |
| self._ShellCmdWithRetry(self._ssh_cmd + remote_cmd) |
| |
| @utils.TimeExecute(function_description="Uploading local image") |
| def _UploadArtifacts(self, |
| cvd_user, |
| local_image_artifact, |
| cvd_host_package_artifact): |
| """Upload local image and avd local host package to instance. |
| |
| Args: |
| cvd_user: A string, user upload the artifacts to instance. |
| local_image_artifact: A string, path to local image. |
| cvd_host_package_artifact: A string, path to cvd host package. |
| """ |
| # local image |
| remote_cmd = ("\"sudo su -c '/usr/bin/install_zip.sh .' - '%s'\" < %s" % |
| (cvd_user, local_image_artifact)) |
| logger.debug("remote_cmd:\n %s", remote_cmd) |
| self._ShellCmdWithRetry(self._ssh_cmd + remote_cmd) |
| |
| # host_package |
| remote_cmd = ("\"sudo su -c 'tar -x -z -f -' - '%s'\" < %s" % |
| (cvd_user, cvd_host_package_artifact)) |
| logger.debug("remote_cmd:\n %s", remote_cmd) |
| self._ShellCmdWithRetry(self._ssh_cmd + remote_cmd) |
| |
| def _LaunchCvd(self, cvd_user, hw_property): |
| """Launch CVD.""" |
| lunch_cvd_args = _CMD_LAUNCH_CVD_ARGS % ( |
| hw_property["cpu"], |
| hw_property["x_res"], |
| hw_property["y_res"], |
| hw_property["dpi"], |
| hw_property["memory"], |
| hw_property["disk"]) |
| remote_cmd = ("\"sudo su -c 'bin/launch_cvd %s>&/dev/ttyS0&' - '%s'\"" % |
| (lunch_cvd_args, cvd_user)) |
| logger.debug("remote_cmd:\n %s", remote_cmd) |
| subprocess.Popen(self._ssh_cmd + remote_cmd, shell=True) |
| |
| |
| class LocalImageRemoteInstance(base_avd_create.BaseAVDCreate): |
| """Create class for a local image remote instance AVD. |
| |
| Attributes: |
| local_image_artifact: A string, path to local image. |
| cvd_host_package_artifact: A string, path to cvd host package. |
| """ |
| |
| def __init__(self): |
| """LocalImageRemoteInstance initialize.""" |
| self.cvd_host_package_artifact = None |
| |
| def VerifyArtifactsExist(self, local_image_dir): |
| """Verify required cuttlefish image artifacts exists. |
| |
| Arsg: |
| local_image_dir: A string, path to check the artifacts. |
| """ |
| self.cvd_host_package_artifact = self.VerifyHostPackageArtifactsExist( |
| local_image_dir) |
| |
| def VerifyHostPackageArtifactsExist(self, local_image_dir): |
| """Verify the host package exists and return its path. |
| |
| Look for the host package in local_image_dir (when we download the |
| image artifacts from Android Build into 1 folder), if we can't find |
| it, look in the dist dir (if the user built the image locally). |
| |
| Args: |
| local_image_dir: A string, path to check for the host package. |
| |
| Return: |
| A string, the path to the host package. |
| """ |
| dirs_to_check = [local_image_dir] |
| dist_dir = utils.GetDistDir() |
| if dist_dir: |
| dirs_to_check.append(dist_dir) |
| cvd_host_package_artifact = self.GetCvdHostPackage(dirs_to_check) |
| logger.debug("cvd host package: %s", cvd_host_package_artifact) |
| return cvd_host_package_artifact |
| |
| @staticmethod |
| def GetCvdHostPackage(paths): |
| """Get cvd host package path. |
| |
| Args: |
| paths: A list, holds the paths to check for the host package. |
| |
| Returns: |
| String, full path of cvd host package. |
| |
| Raises: |
| errors.GetCvdLocalHostPackageError: Can't find cvd host package. |
| """ |
| for path in paths: |
| cvd_host_package = os.path.join(path, _CVD_HOST_PACKAGE) |
| if os.path.exists(cvd_host_package): |
| return cvd_host_package |
| raise errors.GetCvdLocalHostPackageError, ( |
| "Can't find the cvd host package (Try building with 'm dist'): \n%s" |
| % '\n'.join(paths)) |
| |
| @utils.TimeExecute(function_description="Total time: ", |
| print_before_call=False, print_status=False) |
| def _CreateAVD(self, avd_spec): |
| """Create the AVD. |
| |
| Args: |
| avd_spec: AVDSpec object that tells us what we're going to create. |
| """ |
| self.VerifyArtifactsExist(avd_spec.local_image_dir) |
| device_factory = RemoteInstanceDeviceFactory( |
| avd_spec, |
| avd_spec.local_image_artifact, |
| self.cvd_host_package_artifact) |
| report = common_operations.CreateDevices( |
| "create_cf", avd_spec.cfg, device_factory, avd_spec.num, |
| report_internal_ip=avd_spec.report_internal_ip, |
| autoconnect=avd_spec.autoconnect) |
| # Launch vnc client if we're auto-connecting. |
| if avd_spec.autoconnect: |
| utils.LaunchVNCFromReport(report, avd_spec) |
| return report |