#!/usr/bin/python

# reconf-inetd - reconfigure and restart inetd
# Copyright (C) 2011 Serafeim Zanikolas <sez@debian.org>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

import shutil
import os
import re
import sys
import subprocess
import logging
from cStringIO import StringIO
from tempfile import mkdtemp, TemporaryFile
from glob import glob
import commands

import reconf_inetd

from lettuce import *

test_data_dir = './features/data'
default_inetd_conf = os.path.join(test_data_dir, 'inetd.conf')
reconf_script_fname = './reconf_inetd.py'
service_spec_re = '([^,]+),([^,]+),([^,]+)'
DEFAULT_SRV_ARGS = '-d -l'

@before.each_scenario
def before(sc):
    world.scc.logger = logging.getLogger()
    handler = logging.FileHandler('steps.log')
    world.scc.logger.addHandler(handler)
    world.scc.logger.setLevel(logging.DEBUG)

    world.scc.test_data_dir = test_data_dir
    world.scc.default_inetd_conf = default_inetd_conf
    world.scc.tempdir = mkdtemp()
    world.scc.pseudo_root = os.path.join(world.scc.tempdir, 'root')

    world.scc.inetd_conf_fname = os.path.join(world.scc.tempdir, 'inetd.conf')
    world.scc.reconf_fragments_dir = os.path.join(world.scc.tempdir, 'reconf_fragments')
    world.scc.shadow_fragments_dir = os.path.join(world.scc.tempdir, 'shadow_fragments')

    os.mkdir(world.scc.reconf_fragments_dir)
    os.mkdir(world.scc.shadow_fragments_dir)

def load_inetd_service_container():
    container = reconf_inetd.InetdServiceContainer(world.scc.logger)
    world.scc.logger.debug('loading: %s' % world.scc.inetd_conf_fname)
    with open(world.scc.inetd_conf_fname) as inetd_conf_fd:
        container.load_service_entries(inetd_conf_fd)
    return container

def create_inetd_service_container():
    shutil.copy(world.scc.default_inetd_conf, world.scc.inetd_conf_fname)
    return load_inetd_service_container()

def create_shadow_fragment_container():
    shadow_files = glob('%s/*' % world.scc.shadow_fragments_dir)
    return reconf_inetd.XFragmentContainer(world.scc.logger, shadow_files)

def prefix_paths(paths, prefix):
    return ['%s/%s' % (prefix, path) for path in paths.split()]

def server_path_and_args(server_path):
    """return a dictionary based on a one or two-path string"""
    attrs = {}
    server_paths = prefix_paths(server_path, world.scc.pseudo_root)
    if len(server_paths) > 1:
        attrs['server_args'] = server_paths[1]
        attrs['server'] = server_paths[0]
    else:
        attrs['server'] = server_paths[0]

    if world.scc.default_srv_args:
        if attrs.has_key('server_args'):
            attrs['server_args'] += ' ' + world.scc.default_srv_args
        else:
            attrs['server_args'] = world.scc.default_srv_args
    world.scc.logger.debug('server_paths: "%s"' % server_paths)
    world.scc.logger.debug('server: "%s"' % attrs['server'])
    world.scc.logger.debug('server_args: "%s"' % attrs['server_args'])
    if attrs['server'].endswith('/tcpd'):
        attrs['flags'] = 'nameinargs'
    return attrs

def make_service(name, protocol, server_path,
                 srv_status=reconf_inetd.InetdService.ENABLED):
    if protocol.startswith('tcp'):
        socket_type = 'stream'
    else:
        socket_type = 'dgram'
    attrs = { 'service' : name,
              'socket_type' : socket_type,
              'wait' : 'nowait',
              'protocol' : protocol
    }
    attrs = dict(attrs.items() + server_path_and_args(server_path).items())

    # special case: server_path is /usr/sbin/tcpd /path/to/server

    logger = logging.getLogger()
    lineno = 1
    srv = reconf_inetd.InetdService(attrs, lineno, logger)
    srv.set_status(srv_status)
    return srv

def create_inetd_conf_without_entry(srv_name, protocol, srv_path):
    world.scc.service = make_service(srv_name, protocol, srv_path)
    world.scc.inetd_service_container = create_inetd_service_container()
    assert not world.scc.inetd_service_container.has_matching_entry(world.scc.service)

def write_file(name, content):
    with open(name, 'w') as fd:
        fd.write(content)

def create_inetd_conf_with_entry(srv_name, protocol, srv_path, srv_status):
    world.scc.service = make_service(srv_name, protocol, srv_path, srv_status)
    world.scc.inetd_service_container = create_inetd_service_container()

    fd = StringIO(str(world.scc.service))
    world.scc.inetd_service_container.load_service_entries(fd)

    # for debugging
    #write_file('/tmp/srv', str(world.scc.service))
    #world.scc.inetd_service_container.persist('/tmp/inetd.conf')

    assert world.scc.inetd_service_container.has_matching_entry(world.scc.service)
    world.scc.logger.debug('storing "%s" to %s' % (str(world.scc.service),
                                                  world.scc.inetd_conf_fname))
    shadow_files = glob('%s/*' % world.scc.shadow_fragments_dir)
    shadow_container =  reconf_inetd.XFragmentContainer(world.scc.logger,
                                                        shadow_files)
    world.scc.inetd_service_container.persist(world.scc.inetd_conf_fname)

@step("an inetd.conf file with (an? \S+) entry %s" % service_spec_re)
def an_inetd_conf_file_with_a_entry(step, entry_status, srv_name, protocol, srv_path):
    entry_status_key = entry_status.split()[1] # omit a/at article
    status_map = {
        'enabled' : reconf_inetd.InetdService.ENABLED,
        'maintainer_disabled' : reconf_inetd.InetdService.MAINT_DISABLED,
        'user_disabled' : reconf_inetd.InetdService.USER_DISABLED,
        'missing' : 'missing'
    }
    entry_status_flag = status_map.get(entry_status_key)
    world.scc.default_srv_args = DEFAULT_SRV_ARGS

    world.scc.logger.debug('srv name: %s; protocol: %s; srv_path: %s' %
            (srv_name, protocol, srv_path))
    if entry_status_flag == 'missing':
        create_inetd_conf_without_entry(srv_name, protocol, srv_path)
    elif entry_status_flag is None:
        raise Exception('invalid entry status: %s' % entry_status_flag)
    else:
        create_inetd_conf_with_entry(srv_name, protocol, srv_path,
                                     entry_status_flag)

@step('a matching server file that exists')
def a_matching_server_file_that_exists(step):
    server_path = world.scc.service.get_server()
    server_dir = os.path.dirname(server_path)
    os.path.exists(server_dir) or os.makedirs(server_dir)
    open(server_path, 'w').close()

@step('a matching server file that does not exist')
def assert_missing_server_file(step):
    server_path = world.scc.service.get_server()
    assert not os.path.exists(server_path)

@step('a matching shadow fragment with identical server arguments for the %s inetd.conf entry' % service_spec_re)
def create_shadow_fragment_file(step, srv_name, protocol, srv_path):
    # TODO: filename should include protocol and server_path (replacing / with
    # smth else)
    fragment_filename = os.path.join(world.scc.shadow_fragments_dir, srv_name)
    fragment = reconf_inetd.XFragment(srv_name, world.scc.logger, fragment_filename)
    # TODO do this in a cleaner way
    fragment.attrs['socket_type'] = world.scc.service.attrs['socket_type']
    fragment.attrs['wait'] = 'no'
    fragment.attrs['user'] = 'root'
    fragment.attrs['protocol'] = protocol

    fragment.attrs = dict(fragment.attrs.items() + server_path_and_args(srv_path).items())
    fragment.store(fragment_filename)

    fd = StringIO(str(fragment.to_inetd()))
    world.scc.logger.debug('to_inetd:\n%s' % fragment.to_inetd())
    world.scc.inetd_service_container.load_service_entries(fd)
    assert world.scc.inetd_service_container.has_matching_entry_with_identical_srv_args(fragment)

# TODO do not make any assumptions about file names of shadow fragments; parse
# their contents instead
@step('no matching shadow fragment')
def and_no_matching_shadow_fragment(step):
    srv_name = world.scc.service.get_name()
    fragment_filename = os.path.join(world.scc.shadow_fragments_dir, srv_name)
    assert not os.path.exists(fragment_filename)

@step('a matching reconf-inetd fragment for the service %s' % service_spec_re)
def create_inetd_conf_fragment_file(step, srv_name, protocol, server_path):
    # TODO: filename should include protocol and server_path (replacing / with
    # smth else)
    fragment_filename = os.path.join(world.scc.reconf_fragments_dir, srv_name)
    fragment = reconf_inetd.XFragment(srv_name, world.scc.logger, fragment_filename)
    # TODO do this in a cleaner way
    fragment.attrs['socket_type'] = world.scc.service.attrs['socket_type']
    fragment.attrs['wait'] = 'no'
    fragment.attrs['user'] = 'root'
    fragment.attrs['protocol'] = protocol

    fragment.attrs = dict(fragment.attrs.items() + server_path_and_args(server_path).items())
    fragment.store(fragment_filename)

@step('no matching reconf-inetd fragment for the service %s' % service_spec_re)
def assert_missing_fragment_file(step, srv_name, protocol, srv_path):
    # TODO: filename should include protocol and server_path (replacing / with
    # smth else)
    fragment_filename = os.path.join(world.scc.reconf_fragments_dir, srv_name)
    assert not os.path.exists(fragment_filename)

@step('a new entry is added to inetd.conf for service %s' % service_spec_re)
def check_inetd_conf_contains_service(step, srv_name, protocol, srv_path):
    service = make_service(srv_name, protocol, srv_path)
    inetd_service_container = load_inetd_service_container()
    assert inetd_service_container.has_matching_entry(service)

@step('the %s service entry is removed from inetd.conf' % service_spec_re)
def check_service_removal(step, srv_name, protocol, srv_path):
    service = make_service(srv_name, protocol, srv_path)
    # reload service container
    inetd_service_container = load_inetd_service_container()
    assert not inetd_service_container.has_matching_entry(service), \
            dump(world.scc.inetd_conf_fname)

@step('the %s service entry in inetd.conf is enabled' % service_spec_re)
def check_service_enable(step, srv_name, protocol, srv_path):
    service = make_service(srv_name, protocol, srv_path)
    inetd_service_container = load_inetd_service_container()
    assert inetd_service_container.has_matching_enabled_entry(service), \
            dump(world.scc.inetd_conf_fname)

@step('I run reconf-inetd')
def run_reconf_inetd(step):
    world.scc.inetd_conf_orig_fname = '%s.orig' % world.scc.default_inetd_conf
    shutil.copy(world.scc.inetd_conf_fname, world.scc.inetd_conf_orig_fname)
    env = {
        'UPDATE_INETD_FAKE_IT' : '1',
        'RECONF_INETD_LOGLEVEL' : str(logging.DEBUG),
        'RECONF_INETD_LOG' : './features.log',
        'INETD_CONF_FILENAME' : world.scc.inetd_conf_fname,
        'RECONF_INETD_FRAGMENTS_DIR' : world.scc.reconf_fragments_dir,
        'SHADOW_FRAGMENTS_DIR' : world.scc.shadow_fragments_dir
    }
    world.scc.logger.debug('env: ' + str(env))
    args = [reconf_script_fname, '--verbose']
    output_fd = TemporaryFile('rw')
    job = subprocess.Popen(args, env=env, stdout=output_fd,
                           stderr=subprocess.STDOUT)
    job.wait()
    output_fd.seek(0)
    world.scc.invocation_output = output_fd.read()
    world.scc.logger.debug('stdout/stderr:\n%s' % world.scc.invocation_output)

@step("inetd.conf must remain unchanged")
def check_no_changes(step):
    cmd = 'diff %s %s' % (world.scc.inetd_conf_orig_fname,
                          world.scc.inetd_conf_fname)
    world.scc.logger.debug(cmd)
    retcode, output = commands.getstatusoutput(cmd)
    assert retcode == 0, output

@after.each_scenario
def cleanup(sc):
    shutil.rmtree(world.scc.tempdir)

def dump(f):
    print open(f).read()

@step(u'And a matching shadow fragment with different server arguments for the %s inetd.conf entry' % service_spec_re)
def create_shadow_fragment_file_with_diff_args(step, srv_name, protocol, srv_path):
    # TODO: filename should include protocol and server_path (replacing / with
    # smth else)
    fragment_filename = os.path.join(world.scc.shadow_fragments_dir, srv_name)
    fragment = reconf_inetd.XFragment(srv_name, world.scc.logger, fragment_filename)
    # TODO do this in a cleaner way
    fragment.attrs['socket_type'] = world.scc.service.attrs['socket_type']
    fragment.attrs['wait'] = 'no'
    fragment.attrs['user'] = 'root'
    fragment.attrs['protocol'] = protocol

    fragment.attrs = dict(fragment.attrs.items() + server_path_and_args(srv_path).items())
    if fragment.attrs.has_key('server_args'):
        fragment.attrs['server_args'] += ' -d some-other-value -v'
    else:
        fragment.attrs['server_args'] = '-d some-other-value -v'
    fragment.store(fragment_filename)

    assert world.scc.inetd_service_container.has_matching_entry(fragment)
    assert not world.scc.inetd_service_container.has_matching_entry_with_identical_srv_args(fragment)

def service_has_matching_shadow_fragment(srv_name, protocol, srv_path):
    shadow_container = create_shadow_fragment_container()
    world.scc.default_srv_args = DEFAULT_SRV_ARGS

    return shadow_container.has_matching_entry(world.scc.service)

@step(u'And the matching shadow fragment with identical server arguments for %s is removed' % service_spec_re)
def assert_no_matching_shadow_fragment(step, srv_name, protocol, srv_path):
    assert not service_has_matching_shadow_fragment(srv_name, protocol,
                                                    srv_path)

@step(u'And a matching shadow fragment with identical server arguments for %s is created' % service_spec_re)
def assert_matching_fragment_exists(step, srv_name, protocol, srv_path):
    assert service_has_matching_shadow_fragment(srv_name, protocol, srv_path)

@step("inetd is restarted")
def assert_restart_or_sighup(step):
    assert "inetd via invoke-rc.d" in world.scc.invocation_output
