1#!/usr/bin/env python
2#
3# Copyright 2016 - 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.
16
17"""Kernel Swapper.
18
19This class manages swapping kernel images for a Cloud Android instance.
20"""
21import os
22import subprocess
23
24from acloud.public import errors
25from acloud.public import report
26from acloud.internal.lib import android_build_client
27from acloud.internal.lib import android_compute_client
28from acloud.internal.lib import auth
29from acloud.internal.lib import gstorage_client
30from acloud.internal.lib import utils
31
32ALL_SCOPES = ' '.join([android_build_client.AndroidBuildClient.SCOPE,
33                       gstorage_client.StorageClient.SCOPE,
34                       android_compute_client.AndroidComputeClient.SCOPE])
35
36# ssh flags used to communicate with the Cloud Android instance.
37SSH_FLAGS = [
38    '-q', '-o UserKnownHostsFile=/dev/null', '-o "StrictHostKeyChecking no"',
39    '-o ServerAliveInterval=10'
40]
41
42# Shell commands run on target.
43MOUNT_CMD = ('if mountpoint -q /boot ; then umount /boot ; fi ; '
44             'mount -t ext4 /dev/block/sda1 /boot')
45REBOOT_CMD = 'nohup reboot > /dev/null 2>&1 &'
46
47
48class KernelSwapper(object):
49    """A class that manages swapping a kernel image on a Cloud Android instance.
50
51    Attributes:
52        _compute_client: AndroidCopmuteClient object, manages AVD.
53        _instance_name: string, name of Cloud Android Instance.
54        _target_ip: string, IP address of Cloud Android instance.
55        _ssh_flags: string list, flags to be used with ssh and scp.
56    """
57
58    def __init__(self, cfg, instance_name):
59        """Initialize.
60
61        Args:
62            cfg: AcloudConfig object, used to create credentials.
63            instance_name: string, instance name.
64        """
65        credentials = auth.CreateCredentials(cfg, ALL_SCOPES)
66        self._compute_client = android_compute_client.AndroidComputeClient(
67            cfg, credentials)
68        # Name of the Cloud Android instance.
69        self._instance_name = instance_name
70        # IP of the Cloud Android instance.
71        self._target_ip = self._compute_client.GetInstanceIP(instance_name)
72
73    def SwapKernel(self, local_kernel_image):
74        """Swaps the kernel image on target AVD with given kernel.
75
76        Mounts boot image containing the kernel image to the filesystem, then
77        overwrites that kernel image with a new kernel image, then reboots the
78        Cloud Android instance.
79
80        Args:
81            local_kernel_image: string, local path to a kernel image.
82
83        Returns:
84            A Report instance.
85        """
86        r = report.Report(command='swap_kernel')
87        try:
88            self._ShellCmdOnTarget(MOUNT_CMD)
89            self.PushFile(local_kernel_image, '/boot')
90            self.RebootTarget()
91        except subprocess.CalledProcessError as e:
92            r.AddError(str(e))
93            r.SetStatus(report.Status.FAIL)
94            return r
95        except errors.DeviceBootTimeoutError as e:
96            r.AddError(str(e))
97            r.SetStatus(report.Status.BOOT_FAIL)
98            return r
99
100        r.SetStatus(report.Status.SUCCESS)
101        return r
102
103    def PushFile(self, src_path, dest_path):
104        """Pushes local file to target Cloud Android instance.
105
106        Args:
107            src_path: string, local path to file to be pushed.
108            dest_path: string, path on target where to push the file to.
109
110        Raises:
111            subprocess.CalledProcessError: see _ShellCmd.
112        """
113        cmd = 'scp %s %s root@%s:%s' % (' '.join(SSH_FLAGS), src_path,
114                                        self._target_ip, dest_path)
115        self._ShellCmd(cmd)
116
117    def RebootTarget(self):
118        """Reboots the target Cloud Android instance and waits for boot.
119
120        Raises:
121            subprocess.CalledProcessError: see _ShellCmd.
122            errors.DeviceBootTimeoutError: if booting times out.
123        """
124        self._ShellCmdOnTarget(REBOOT_CMD)
125        self._compute_client.WaitForBoot(self._instance_name)
126
127    def _ShellCmdOnTarget(self, target_cmd):
128        """Runs a shell command on target Cloud Android instance.
129
130        Args:
131            target_cmd: string, shell command to be run on target.
132
133        Raises:
134            subprocess.CalledProcessError: see _ShellCmd.
135        """
136        ssh_cmd = 'ssh %s root@%s' % (' '.join(SSH_FLAGS), self._target_ip)
137        host_cmd = ' '.join([ssh_cmd, '"%s"' % target_cmd])
138        self._ShellCmd(host_cmd)
139
140    def _ShellCmd(self, host_cmd):
141        """Runs a shell command on host device.
142
143        Args:
144            host_cmd: string, shell command to be run on host.
145
146        Raises:
147            subprocess.CalledProcessError: For any non-zero return code of
148                                           host_cmd.
149        """
150        utils.Retry(
151            retry_checker=lambda e: isinstance(e, subprocess.CalledProcessError),
152            max_retries=2,
153            functor=lambda cmd: subprocess.check_call(cmd, shell=True),
154            cmd=host_cmd)
155