#!/usr/bin/env python3

# Copyright 2016 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 os
import random
import signal
import socket
import sys
import time

from neutron_lib import constants as n_const
from oslo_config import cfg

from neutron.agent.linux import daemon

UNIX_FAMILY = 'UNIX'

OPTS = [
    cfg.IntOpt('num_children',
               short='n',
               default=0,
               help='Number of children to spawn',
               required=False),
    cfg.StrOpt('family',
               short='f',
               default=n_const.IPv4,
               choices=[n_const.IPv4, n_const.IPv6, UNIX_FAMILY],
               help='Listen socket family (%(v4)s, %(v6)s or %(unix)s)' %
                     {
                         'v4': n_const.IPv4,
                         'v6': n_const.IPv6,
                         'unix': UNIX_FAMILY
                     },
               required=False),
    cfg.StrOpt('proto',
               short='p',
               default=n_const.PROTO_NAME_TCP,
               choices=[n_const.PROTO_NAME_TCP, n_const.PROTO_NAME_UDP],
               help='Protocol (%(tcp)s or %(udp)s)' %
                     {
                         'tcp': n_const.PROTO_NAME_TCP,
                         'udp': n_const.PROTO_NAME_UDP
                     },
               required=False),
    cfg.BoolOpt('parent_listen',
               short='pl',
               default=True,
               help='Parent process must listen too',
               required=False),
    cfg.BoolOpt('ignore_sigterm',
                short='i',
                default=False,
                help='Ignore SIGTERM',
                required=False)
]


class ProcessSpawn(daemon.Daemon):
    """This class is part of the functional test of the netns_cleanup module.

    It allows spawning processes that listen on random ports either on
    tcp(6), udp(6) or unix sockets. Also it allows handling or ignoring
    SIGTERM to check whether the cleanup works as expected by getting rid
    of the spawned processes.
    """

    MAX_BIND_RETRIES = 64

    DCT_FAMILY = {
        n_const.IPv4: socket.AF_INET,
        n_const.IPv6: socket.AF_INET6,
        UNIX_FAMILY: socket.AF_UNIX
    }
    DCT_PROTO = {
        n_const.PROTO_NAME_TCP: socket.SOCK_STREAM,
        n_const.PROTO_NAME_UDP: socket.SOCK_DGRAM,
    }

    def __init__(self, pidfile=None,
                 family=n_const.IPv4,
                 proto=n_const.PROTO_NAME_TCP,
                 ignore_sigterm=False, num_children=0,
                 parent_must_listen=True):
        self.family = family
        self.proto = proto
        self.ignore_sigterm = ignore_sigterm
        self.num_children = num_children
        self.listen_socket = None
        self.parent_must_listen = parent_must_listen
        self.child_pids = []

        super(ProcessSpawn, self).__init__(pidfile)

    def start_listening(self):
        socket_family = self.DCT_FAMILY[self.family]
        socket_type = self.DCT_PROTO[self.proto]

        self.listen_socket = socket.socket(socket_family, socket_type)

        # Set a different seed per process to increase randomness
        random.seed(os.getpid())

        # Try to listen in a random port which is not currently in use
        retries = 0
        while retries < ProcessSpawn.MAX_BIND_RETRIES:
            # NOTE(dalvarez): not finding a free port on a freshly created
            # namespace is very unlikely but if problems show up, retries can
            # be increased to avoid tests failing
            try:
                if self.family == UNIX_FAMILY:
                    self.listen_socket.bind('')
                else:
                    # Pick a non privileged port
                    port = random.randint(1024, 65535)
                    self.listen_socket.bind(('', port))
            except socket.error:
                retries += 1
            else:
                if n_const.PROTO_NAME_TCP in self.proto:
                    self.listen_socket.listen(0)
                break

    def do_sleep(self):
        while True:
            time.sleep(10)

    def run(self):
        # Spawn as many children as requested
        children = []
        while len(children) != self.num_children:
            child_pid = os.fork()
            if child_pid == 0:
                # Listen and do nothing else
                self.start_listening()
                self.do_sleep()
                return
            children.append(child_pid)

        # Install a SIGTERM handler if requested
        handler = (
            signal.SIG_IGN if self.ignore_sigterm else self.sigterm_handler)
        signal.signal(signal.SIGTERM, handler)

        self.child_pids = children
        if self.parent_must_listen:
            self.start_listening()
        self.do_sleep()

    def sigterm_handler(self, signum, frame):
        if self.listen_socket:
            self.listen_socket.close()
        for child in self.child_pids:
            try:
                os.kill(child, signal.SIGTERM)
            except OSError:
                pass
        sys.exit(0)


def main():
    cfg.CONF.register_cli_opts(OPTS)
    cfg.CONF(project='neutron', default_config_files=[])
    proc_spawn = ProcessSpawn(num_children=cfg.CONF.num_children,
                      family=cfg.CONF.family,
                      proto=cfg.CONF.proto,
                      parent_must_listen=cfg.CONF.parent_listen,
                      ignore_sigterm=cfg.CONF.ignore_sigterm)
    proc_spawn.start()


if __name__ == "__main__":
    main()
