#!/usr/bin/env python
#
# Copyright 2016 - 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.
"""Kernel Swapper.

This class manages swapping kernel images for a Cloud Android instance.
"""
import subprocess

from acloud import errors
from acloud.public import report
from acloud.internal.lib import android_compute_client
from acloud.internal.lib import auth
from acloud.internal.lib import utils


# ssh flags used to communicate with the Cloud Android instance.
SSH_FLAGS = [
    '-q', '-o UserKnownHostsFile=/dev/null', '-o "StrictHostKeyChecking no"',
    '-o ServerAliveInterval=10'
]

# Shell commands run on target.
MOUNT_CMD = ('if mountpoint -q /boot ; then umount /boot ; fi ; '
             'mount -t ext4 /dev/block/sda1 /boot')
REBOOT_CMD = 'nohup reboot > /dev/null 2>&1 &'


class KernelSwapper():
    """A class that manages swapping a kernel image on a Cloud Android instance.

    Attributes:
        _compute_client: AndroidCopmuteClient object, manages AVD.
        _instance_name: tring, name of Cloud Android Instance.
        _target_ip: string, IP address of Cloud Android instance.
        _ssh_flags: string list, flags to be used with ssh and scp.
    """

    def __init__(self, cfg, instance_name):
        """Initialize.

        Args:
            cfg: AcloudConfig object, used to create credentials.
            instance_name: string, instance name.
        """
        credentials = auth.CreateCredentials(cfg)
        self._compute_client = android_compute_client.AndroidComputeClient(
            cfg, credentials)
        # Name of the Cloud Android instance.
        self._instance_name = instance_name
        # IP of the Cloud Android instance.
        self._target_ip = self._compute_client.GetInstanceIP(instance_name)

    def SwapKernel(self, local_kernel_image):
        """Swaps the kernel image on target AVD with given kernel.

        Mounts boot image containing the kernel image to the filesystem, then
        overwrites that kernel image with a new kernel image, then reboots the
        Cloud Android instance.

        Args:
            local_kernel_image: string, local path to a kernel image.

        Returns:
            A Report instance.
        """
        reboot_image = report.Report(command='swap_kernel')
        try:
            self._ShellCmdOnTarget(MOUNT_CMD)
            self.PushFile(local_kernel_image, '/boot')
            self.RebootTarget()
        except subprocess.CalledProcessError as e:
            reboot_image.AddError(str(e))
            reboot_image.SetStatus(report.Status.FAIL)
            return reboot_image
        except errors.DeviceBootError as e:
            reboot_image.AddError(str(e))
            reboot_image.SetStatus(report.Status.BOOT_FAIL)
            return reboot_image

        reboot_image.SetStatus(report.Status.SUCCESS)
        return reboot_image

    def PushFile(self, src_path, dest_path):
        """Pushes local file to target Cloud Android instance.

        Args:
            src_path: string, local path to file to be pushed.
            dest_path: string, path on target where to push the file to.

        Raises:
            subprocess.CalledProcessError: see _ShellCmd.
        """
        cmd = 'scp %s %s root@%s:%s' % (' '.join(SSH_FLAGS), src_path,
                                        self._target_ip, dest_path)
        self._ShellCmd(cmd)

    def RebootTarget(self):
        """Reboots the target Cloud Android instance and waits for boot.

        Raises:
            subprocess.CalledProcessError: see _ShellCmd.
            errors.DeviceBootError: if target fails to boot.
        """
        self._ShellCmdOnTarget(REBOOT_CMD)
        self._compute_client.WaitForBoot(self._instance_name)

    def _ShellCmdOnTarget(self, target_cmd):
        """Runs a shell command on target Cloud Android instance.

        Args:
            target_cmd: string, shell command to be run on target.

        Raises:
            subprocess.CalledProcessError: see _ShellCmd.
        """
        ssh_cmd = 'ssh %s root@%s' % (' '.join(SSH_FLAGS), self._target_ip)
        host_cmd = ' '.join([ssh_cmd, '"%s"' % target_cmd])
        self._ShellCmd(host_cmd)

    @staticmethod
    def _ShellCmd(host_cmd):
        """Runs a shell command on host device.

        Args:
            host_cmd: string, shell command to be run on host.

        Raises:
            subprocess.CalledProcessError: For any non-zero return code of
                                           host_cmd.
        """
        utils.Retry(
            retry_checker=lambda e: isinstance(e, subprocess.CalledProcessError),
            max_retries=2,
            functor=lambda cmd: subprocess.check_call(cmd, shell=True),
            sleep_multiplier=0,
            retry_backoff_factor=1,
            cmd=host_cmd)
