# vim: tabstop=4 shiftwidth=4 softtabstop=4

# Copyright 2011 OpenStack, LLC
# All Rights Reserved.
#
#    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.

"""
Base test class for running non-stubbed tests (functional tests)

The FunctionalTest class contains helper methods for starting the API
and Registry server, grabbing the logs of each, cleaning up pidfiles,
and spinning down the servers.
"""

import datetime
import os
import random
import shutil
import signal
import socket
import tempfile
import time
import unittest

from tests.utils import execute, get_unused_port


class FunctionalTest(unittest.TestCase):

    """
    Base test class for any test that wants to test the actual
    servers and clients and not just the stubbed out interfaces
    """

    def setUp(self):

        self.test_id = random.randint(0, 100000)
        self.test_dir = os.path.join("/", "tmp", "test.%d" % self.test_id)

        self.api_port = get_unused_port()
        self.api_pid_file = os.path.join(self.test_dir,
                                         "glance-api.pid")
        self.api_log_file = os.path.join(self.test_dir, "apilog")

        self.registry_port = get_unused_port()
        self.registry_pid_file = ("/tmp/test.%d/glance-registry.pid"
                                  % self.test_id)
        self.registry_log_file = os.path.join(self.test_dir, "registrylog")

        self.image_dir = "/tmp/test.%d/images" % self.test_id

        self.sql_connection = os.environ.get('GLANCE_SQL_CONNECTION',
                                             "sqlite://")
        self.pid_files = [self.api_pid_file,
                          self.registry_pid_file]
        self.files_to_destroy = []

    def tearDown(self):
        self.cleanup()

    def cleanup(self):
        """
        Makes sure anything we created or started up in the
        tests are destroyed or spun down
        """

        for pid_file in self.pid_files:
            if os.path.exists(pid_file):
                pid = int(open(pid_file).read().strip())
                try:
                    os.killpg(pid, signal.SIGTERM)
                except:
                    pass  # Ignore if the process group is dead
                os.unlink(pid_file)

        for f in self.files_to_destroy:
            if os.path.exists(f):
                os.unlink(f)

    def start_servers(self, **kwargs):
        """
        Starts the API and Registry servers (bin/glance-api and
        bin/glance-registry) on unused ports and returns a tuple
        of the (api_port, registry_port, conf_file_name).

        Any kwargs passed to this method will override the configuration
        value in the conf file used in starting the servers.
        """
        self.cleanup()

        conf_override = self.__dict__.copy()
        if kwargs:
            conf_override.update(**kwargs)

        # A config file to use just for this test...we don't want
        # to trample on currently-running Glance servers, now do we?

        conf_file = tempfile.NamedTemporaryFile()
        conf_contents = """[DEFAULT]
verbose = True
debug = True

[app:glance-api]
paste.app_factory = glance.server:app_factory
filesystem_store_datadir=%(image_dir)s
default_store = file
bind_host = 0.0.0.0
bind_port = %(api_port)s
registry_host = 0.0.0.0
registry_port = %(registry_port)s
log_file = %(api_log_file)s

[app:glance-registry]
paste.app_factory = glance.registry.server:app_factory
bind_host = 0.0.0.0
bind_port = %(registry_port)s
log_file = %(registry_log_file)s
sql_connection = %(sql_connection)s
sql_idle_timeout = 3600
""" % conf_override
        conf_file.write(conf_contents)
        conf_file.flush()
        self.conf_file_name = conf_file.name

        # Start up the API and default registry server
        cmd = ("./bin/glance-control api start "
               "%(conf_file_name)s --pid-file=%(api_pid_file)s"
               % self.__dict__)
        exitcode, out, err = execute(cmd)

        self.assertEqual(0, exitcode,
                         "Failed to spin up the API server. "
                         "Got: %s" % err)
        self.assertTrue("Starting glance-api with" in out)

        cmd = ("./bin/glance-control registry start "
               "%(conf_file_name)s --pid-file=%(registry_pid_file)s"
               % self.__dict__)
        exitcode, out, err = execute(cmd)

        self.assertEqual(0, exitcode,
                         "Failed to spin up the Registry server. "
                         "Got: %s" % err)
        self.assertTrue("Starting glance-registry with" in out)

        self.wait_for_servers()

        return self.api_port, self.registry_port, self.conf_file_name

    def ping_server(self, port):
        """
        Simple ping on the port. If responsive, return True, else
        return False.

        :note We use raw sockets, not ping here, since ping uses ICMP and
        has no concept of ports...
        """
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        try:
            s.connect(("127.0.0.1", port))
            s.close()
            return True
        except socket.error, e:
            return False

    def wait_for_servers(self, timeout=3):
        """
        Tight loop, waiting for both API and registry server to be
        available on the ports. Returns when both are pingable. There
        is a timeout on waiting for the servers to come up.

        :param timeout: Optional, defaults to 3
        """
        now = datetime.datetime.now()
        timeout_time = now + datetime.timedelta(seconds=timeout)
        while (timeout_time > now):
            if self.ping_server(self.api_port) and\
               self.ping_server(self.registry_port):
                return
            now = datetime.datetime.now()
            time.sleep(0.05)
        self.assertFalse(True, "Failed to start servers.")

    def stop_servers(self):
        """
        Called to stop the started servers in a normal fashion. Note
        that cleanup() will stop the servers using a fairly draconian
        method of sending a SIGTERM signal to the servers. Here, we use
        the glance-control stop method to gracefully shut the server down.
        This method also asserts that the shutdown was clean, and so it
        is meant to be called during a normal test case sequence.
        """

        # Spin down the API and default registry server
        cmd = ("./bin/glance-control api start "
               "%(conf_file_name)s --pid-file=%(api_pid_file)s"
               % self.__dict__)
        exitcode, out, err = execute(cmd)
        self.assertEqual(0, exitcode,
                         "Failed to spin down the API server. "
                         "Got: %s" % err)
        cmd = ("./bin/glance-control registry start "
               "%(conf_file_name)s --pid-file=%(registry_pid_file)s"
               % self.__dict__)
        exitcode, out, err = execute(cmd)
        self.assertEqual(0, exitcode,
                         "Failed to spin down the Registry server. "
                         "Got: %s" % err)

        # If all went well, then just remove the test directory.
        # We only want to check the logs and stuff if something
        # went wrong...
        if os.path.exists(self.test_dir):
            shutil.rmtree(self.test_dir)
