Blob Blame History Raw
# This file is part of cloud-init. See LICENSE file for license information.

"""Base instance."""
import time

import paramiko
from paramiko.ssh_exception import (
    BadHostKeyException, AuthenticationException, SSHException)

from ..util import TargetBase
from tests.cloud_tests import LOG, util


class Instance(TargetBase):
    """Base instance object."""

    platform_name = None
    _ssh_client = None

    def __init__(self, platform, name, properties, config, features):
        """Set up instance.

        @param platform: platform object
        @param name: hostname of instance
        @param properties: image properties
        @param config: image config
        @param features: supported feature flags
        """
        self.platform = platform
        self.name = name
        self.properties = properties
        self.config = config
        self.features = features
        self._tmp_count = 0

        self.ssh_ip = None
        self.ssh_port = None
        self.ssh_key_file = None
        self.ssh_username = 'ubuntu'

    def console_log(self):
        """Instance console.

        @return_value: bytes of this instance’s console
        """
        raise NotImplementedError

    def reboot(self, wait=True):
        """Reboot instance."""
        raise NotImplementedError

    def shutdown(self, wait=True):
        """Shutdown instance."""
        raise NotImplementedError

    def start(self, wait=True, wait_for_cloud_init=False):
        """Start instance."""
        raise NotImplementedError

    def destroy(self):
        """Clean up instance."""
        self._ssh_close()

    def _ssh(self, command, stdin=None):
        """Run a command via SSH."""
        client = self._ssh_connect()

        cmd = util.shell_pack(command)
        fp_in, fp_out, fp_err = client.exec_command(cmd)
        channel = fp_in.channel

        if stdin is not None:
            fp_in.write(stdin)
            fp_in.close()

        channel.shutdown_write()
        rc = channel.recv_exit_status()

        return (fp_out.read(), fp_err.read(), rc)

    def _ssh_close(self):
        if self._ssh_client:
            try:
                self._ssh_client.close()
            except SSHException:
                LOG.warning('Failed to close SSH connection.')
            self._ssh_client = None

    def _ssh_connect(self):
        """Connect via SSH.

        Attempt to SSH to the client on the specific IP and port. If it
        fails in some manner, then retry 2 more times for a total of 3
        attempts; sleeping a few seconds between attempts.
        """
        if self._ssh_client:
            return self._ssh_client

        if not self.ssh_ip or not self.ssh_port:
            raise ValueError("Cannot ssh_connect, ssh_ip=%s ssh_port=%s" %
                             (self.ssh_ip, self.ssh_port))

        client = paramiko.SSHClient()
        client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        private_key = paramiko.RSAKey.from_private_key_file(self.ssh_key_file)

        retries = 3
        while retries:
            try:
                client.connect(username=self.ssh_username,
                               hostname=self.ssh_ip, port=self.ssh_port,
                               pkey=private_key)
                self._ssh_client = client
                return client
            except (ConnectionRefusedError, AuthenticationException,
                    BadHostKeyException, ConnectionResetError, SSHException,
                    OSError):
                retries -= 1
                LOG.debug('Retrying ssh connection on connect failure')
                time.sleep(3)

        ssh_cmd = 'Failed ssh connection to %s@%s:%s after 3 retries' % (
            self.ssh_username, self.ssh_ip, self.ssh_port
        )
        raise util.InTargetExecuteError(b'', b'', 1, ssh_cmd, 'ssh')

    def _wait_for_system(self, wait_for_cloud_init):
        """Wait until system has fully booted and cloud-init has finished.

        @param wait_time: maximum time to wait
        @return_value: None, may raise OSError if wait_time exceeded
        """
        def clean_test(test):
            """Clean formatting for system ready test testcase."""
            return ' '.join(line for line in test.strip().splitlines()
                            if not line.lstrip().startswith('#'))

        boot_timeout = self.config['boot_timeout']
        tests = [self.config['system_ready_script']]
        if wait_for_cloud_init:
            tests.append(self.config['cloud_init_ready_script'])

        formatted_tests = ' && '.join(clean_test(t) for t in tests)
        cmd = ('i=0; while [ $i -lt {time} ] && i=$(($i+1)); do {test} && '
               'exit 0; sleep 1; done; exit 1').format(time=boot_timeout,
                                                       test=formatted_tests)

        end_time = time.time() + boot_timeout
        while True:
            try:
                return_code = self.execute(
                    cmd, rcs=(0, 1), description='wait for instance start'
                )[-1]
                if return_code == 0:
                    break
            except util.InTargetExecuteError:
                LOG.warning("failed to connect via SSH")

            if time.time() < end_time:
                time.sleep(3)
            else:
                raise util.PlatformError('ssh', 'after %ss instance is not '
                                         'reachable' % boot_timeout)

# vi: ts=4 expandtab