# Copyright 2015 Red Hat, Inc.
#
#    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.

import datetime
from distutils import spawn
import os
import re
import signal

import fixtures
from neutron_lib import constants
from neutronclient.common import exceptions as nc_exc
from neutronclient.v2_0 import client
from oslo_log import log as logging
from oslo_utils import fileutils

from neutron.agent.common import async_process
from neutron.agent.linux import ip_lib
from neutron.agent.linux import utils
from neutron.common import utils as common_utils
from neutron.tests import base
from neutron.tests.common import net_helpers
from neutron.tests.fullstack import base as fullstack_base

LOG = logging.getLogger(__name__)
CMD_FOLDER = 'agents'


class ProcessFixture(fixtures.Fixture):
    def __init__(self, test_name, process_name, exec_name, config_filenames,
                 namespace=None, kill_signal=signal.SIGKILL):
        super(ProcessFixture, self).__init__()
        self.test_name = test_name
        self.process_name = process_name
        self.exec_name = exec_name
        self.config_filenames = config_filenames
        self.process = None
        self.kill_signal = kill_signal
        self.namespace = namespace

    def _setUp(self):
        self.start()
        self.addCleanup(self.stop)

    def start(self):
        test_name = base.sanitize_log_path(self.test_name)

        log_dir = os.path.join(fullstack_base.DEFAULT_LOG_DIR, test_name)
        fileutils.ensure_tree(log_dir, mode=0o755)

        timestamp = datetime.datetime.now().strftime("%Y-%m-%d--%H-%M-%S-%f")
        log_file = "%s--%s.log" % (self.process_name, timestamp)
        run_as_root = bool(self.namespace)
        exec_name = (self.exec_name
                     if run_as_root
                     else spawn.find_executable(self.exec_name))
        cmd = [exec_name, '--log-dir', log_dir, '--log-file', log_file]
        for filename in self.config_filenames:
            cmd += ['--config-file', filename]
        self.process = async_process.AsyncProcess(
            cmd, run_as_root=run_as_root, namespace=self.namespace,
            process_name=self.process_name
        )
        self.process.start(block=True)
        LOG.debug("Process started: %s", self.process_name)

    def stop(self, kill_signal=None):
        kill_signal = kill_signal or self.kill_signal
        try:
            self.process.stop(
                block=True, kill_signal=kill_signal,
                kill_timeout=15)
        except async_process.AsyncProcessException as e:
            if "Process is not running" not in str(e):
                raise
        LOG.debug("Process stopped: %s", self.process_name)

    def restart(self, executor=None):
        def _restart():
            self.stop()
            self.start()

        LOG.debug("Restarting process: %s", self.process_name)

        if executor is None:
            _restart()
        else:
            return executor.submit(_restart)

    def process_is_running(self):
        return self.process.is_running

    def process_is_not_running(self):
        return not self.process_is_running()


class RabbitmqEnvironmentFixture(fixtures.Fixture):

    def __init__(self, host="127.0.0.1"):
        super(RabbitmqEnvironmentFixture, self).__init__()
        self.host = host

    def _setUp(self):
        self.user = common_utils.get_rand_name(prefix='user')
        self.password = common_utils.get_rand_name(prefix='pass')
        self.vhost = common_utils.get_rand_name(prefix='vhost')

        self._execute('add_user', self.user, self.password)
        self.addCleanup(self._execute, 'delete_user', self.user)

        self._execute('add_vhost', self.vhost)
        self.addCleanup(self._execute, 'delete_vhost', self.vhost)

        self._execute('set_permissions', '-p', self.vhost, self.user,
                      '.*', '.*', '.*')

    def _execute(self, *args):
        cmd = ['rabbitmqctl']
        cmd.extend(args)
        utils.execute(cmd, run_as_root=True)


class ServiceFixture(fixtures.Fixture):
    def restart(self, executor=None):
        return self.process_fixture.restart(executor=executor)

    def start(self):
        return self.process_fixture.start()

    def stop(self, kill_signal=None):
        return self.process_fixture.stop(kill_signal=kill_signal)


class NeutronServerFixture(ServiceFixture):

    NEUTRON_SERVER = "neutron-server"

    def __init__(self, env_desc, host_desc,
                 test_name, neutron_cfg_fixture, plugin_cfg_fixture,
                 service_cfg_fixtures=None):
        super(NeutronServerFixture, self).__init__()
        self.env_desc = env_desc
        self.host_desc = host_desc
        self.test_name = test_name
        self.neutron_cfg_fixture = neutron_cfg_fixture
        self.plugin_cfg_fixture = plugin_cfg_fixture
        self.service_cfg_fixtures = service_cfg_fixtures

    def _setUp(self):
        config_filenames = [self.neutron_cfg_fixture.filename,
                            self.plugin_cfg_fixture.filename]

        if self.service_cfg_fixtures:
            config_filenames.extend(
                [scf.filename for scf in self.service_cfg_fixtures])

        self.process_fixture = self.useFixture(ProcessFixture(
            test_name=self.test_name,
            process_name=self.NEUTRON_SERVER,
            exec_name=self.NEUTRON_SERVER,
            config_filenames=config_filenames,
            kill_signal=signal.SIGTERM))

        common_utils.wait_until_true(self.server_is_live)

    def server_is_live(self):
        try:
            self.client.list_networks()
            return True
        except nc_exc.NeutronClientException:
            return False

    @property
    def client(self):
        url = ("http://127.0.0.1:%s" %
               self.neutron_cfg_fixture.config.DEFAULT.bind_port)
        return client.Client(auth_strategy="noauth", endpoint_url=url)


class OVSAgentFixture(ServiceFixture):

    def __init__(self, env_desc, host_desc,
                 test_name, neutron_cfg_fixture, agent_cfg_fixture):
        super(OVSAgentFixture, self).__init__()
        self.env_desc = env_desc
        self.host_desc = host_desc
        self.test_name = test_name
        self.neutron_cfg_fixture = neutron_cfg_fixture
        self.neutron_config = self.neutron_cfg_fixture.config
        self.agent_cfg_fixture = agent_cfg_fixture
        self.agent_config = agent_cfg_fixture.config

    def _setUp(self):
        self.br_int = self.useFixture(
            net_helpers.OVSBridgeFixture(
                self.agent_cfg_fixture.get_br_int_name())).bridge

        config_filenames = [self.neutron_cfg_fixture.filename,
                            self.agent_cfg_fixture.filename]

        self.process_fixture = self.useFixture(ProcessFixture(
            test_name=self.test_name,
            process_name=constants.AGENT_PROCESS_OVS,
            exec_name=spawn.find_executable(
                'ovs_agent.py',
                path=os.path.join(fullstack_base.ROOTDIR, CMD_FOLDER)),
            config_filenames=config_filenames,
            kill_signal=signal.SIGTERM))


class PlacementFixture(fixtures.Fixture):

    def __init__(self, env_desc, host_desc, test_name, placement_cfg_fixture):
        super(PlacementFixture, self).__init__()
        self.env_desc = env_desc
        self.host_desc = host_desc
        self.test_name = test_name
        self.placement_cfg_fixture = placement_cfg_fixture
        self.placement_config = self.placement_cfg_fixture.config

    def _setUp(self):
        self.process_fixture = self.useFixture(ProcessFixture(
            test_name=self.test_name,
            process_name='placement',
            exec_name=spawn.find_executable(
                'placement.py', path=os.path.join(fullstack_base.ROOTDIR,
                                                  'servers')
            ),
            config_filenames=[self.placement_cfg_fixture.filename],
            kill_signal=signal.SIGTERM))


class SRIOVAgentFixture(ServiceFixture):

    def __init__(self, env_desc, host_desc,
                 test_name, neutron_cfg_fixture, agent_cfg_fixture):
        super(SRIOVAgentFixture, self).__init__()
        self.env_desc = env_desc
        self.host_desc = host_desc
        self.test_name = test_name
        self.neutron_cfg_fixture = neutron_cfg_fixture
        self.neutron_config = self.neutron_cfg_fixture.config
        self.agent_cfg_fixture = agent_cfg_fixture
        self.agent_config = agent_cfg_fixture.config

    def _setUp(self):
        config_filenames = [self.neutron_cfg_fixture.filename,
                            self.agent_cfg_fixture.filename]
        self.process_fixture = self.useFixture(ProcessFixture(
            test_name=self.test_name,
            process_name=constants.AGENT_PROCESS_NIC_SWITCH,
            exec_name=constants.AGENT_PROCESS_NIC_SWITCH,
            config_filenames=config_filenames,
            kill_signal=signal.SIGTERM))


class LinuxBridgeAgentFixture(ServiceFixture):

    def __init__(self, env_desc, host_desc, test_name,
                 neutron_cfg_fixture, agent_cfg_fixture,
                 namespace=None):
        super(LinuxBridgeAgentFixture, self).__init__()
        self.env_desc = env_desc
        self.host_desc = host_desc
        self.test_name = test_name
        self.neutron_cfg_fixture = neutron_cfg_fixture
        self.neutron_config = self.neutron_cfg_fixture.config
        self.agent_cfg_fixture = agent_cfg_fixture
        self.agent_config = agent_cfg_fixture.config
        self.namespace = namespace

    def _setUp(self):
        config_filenames = [self.neutron_cfg_fixture.filename,
                            self.agent_cfg_fixture.filename]

        self.process_fixture = self.useFixture(
            ProcessFixture(
                test_name=self.test_name,
                process_name=constants.AGENT_PROCESS_LINUXBRIDGE,
                exec_name=constants.AGENT_PROCESS_LINUXBRIDGE,
                config_filenames=config_filenames,
                namespace=self.namespace
            )
        )


class L3AgentFixture(ServiceFixture):

    def __init__(self, env_desc, host_desc, test_name,
                 neutron_cfg_fixture, l3_agent_cfg_fixture,
                 namespace=None):
        super(L3AgentFixture, self).__init__()
        self.env_desc = env_desc
        self.host_desc = host_desc
        self.test_name = test_name
        self.neutron_cfg_fixture = neutron_cfg_fixture
        self.l3_agent_cfg_fixture = l3_agent_cfg_fixture
        self.namespace = namespace

    def _setUp(self):
        self.plugin_config = self.l3_agent_cfg_fixture.config

        config_filenames = [self.neutron_cfg_fixture.filename,
                            self.l3_agent_cfg_fixture.filename]

        # if we execute in namespace as root, then allow rootwrap to find the
        # executable, otherwise construct full path ourselves
        if self.namespace:
            exec_name = 'l3_agent.py'
        else:
            exec_name = spawn.find_executable(
                'l3_agent.py',
                path=os.path.join(fullstack_base.ROOTDIR, CMD_FOLDER))

        self.process_fixture = self.useFixture(
            ProcessFixture(
                test_name=self.test_name,
                process_name=constants.AGENT_PROCESS_L3,
                exec_name=exec_name,
                config_filenames=config_filenames,
                namespace=self.namespace
            )
        )

    def get_namespace_suffix(self):
        return self.plugin_config.DEFAULT.test_namespace_suffix


class DhcpAgentFixture(fixtures.Fixture):

    def __init__(self, env_desc, host_desc, test_name,
                 neutron_cfg_fixture, agent_cfg_fixture, namespace=None):
        super(DhcpAgentFixture, self).__init__()
        self.env_desc = env_desc
        self.host_desc = host_desc
        self.test_name = test_name
        self.neutron_cfg_fixture = neutron_cfg_fixture
        self.agent_cfg_fixture = agent_cfg_fixture
        self.namespace = namespace

    def _setUp(self):
        self.plugin_config = self.agent_cfg_fixture.config

        config_filenames = [self.neutron_cfg_fixture.filename,
                            self.agent_cfg_fixture.filename]

        # if we execute in namespace as root, then allow rootwrap to find the
        # executable, otherwise construct full path ourselves
        if self.namespace:
            exec_name = 'dhcp_agent.py'
        else:
            exec_name = spawn.find_executable(
                'dhcp_agent.py',
                path=os.path.join(fullstack_base.ROOTDIR, CMD_FOLDER))

        self.process_fixture = self.useFixture(
            ProcessFixture(
                test_name=self.test_name,
                process_name=constants.AGENT_PROCESS_DHCP,
                exec_name=exec_name,
                config_filenames=config_filenames,
                namespace=self.namespace
            )
        )
        self.dhcp_namespace_pattern = re.compile(
            r"qdhcp-[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}%s" %
            self.get_namespace_suffix())
        self.addCleanup(self.clean_dhcp_namespaces)

    def get_agent_hostname(self):
        return self.neutron_cfg_fixture.config['DEFAULT']['host']

    def get_namespace_suffix(self):
        return self.plugin_config.DEFAULT.test_namespace_suffix

    def kill(self):
        self.process_fixture.stop()
        self.clean_dhcp_namespaces()

    def clean_dhcp_namespaces(self):
        """Delete all DHCP namespaces created by DHCP agent.

        In some tests for DHCP agent HA agents are killed when handling DHCP
        service for network(s). In such case DHCP namespace is not deleted by
        DHCP agent and such namespaces are found and deleted using agent's
        namespace suffix.
        """

        for namespace in ip_lib.list_network_namespaces():
            if self.dhcp_namespace_pattern.match(namespace):
                try:
                    ip_lib.delete_network_namespace(namespace)
                except RuntimeError:
                    # Continue cleaning even if namespace deletions fails
                    pass
